gabriel / musehub public
test_storage_backends.py python
131 lines 5.1 KB
Raw
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor ⚠ breaking 1 day ago
1 """TDD contract for BlobBackend presign URL rewriting.
2
3 Bug (issue #62 — Phase 1):
4 presign_get does not call _rewrite_presign_url, unlike presign_put and
5 presign_batch. When BLOB_STORAGE_PUBLIC_ENDPOINT differs from
6 BLOB_STORAGE_ENDPOINT (e.g. http://minio:9000 vs http://localhost:9000 in
7 local dev), presign_get returns a URL with the Docker-internal hostname that
8 external clients cannot reach.
9
10 Root cause: backends.py:492 — return value is not wrapped in _rewrite_presign_url.
11
12 Fix: wrap return value the same way presign_put (line 471) and presign_batch
13 (line 517) already do.
14 """
15 from __future__ import annotations
16
17 import asyncio
18 from unittest.mock import MagicMock, patch
19
20 import pytest
21
22 from musehub.storage.backends import BlobBackend
23
24
25 _INTERNAL = "http://minio:9000"
26 _PUBLIC = "http://localhost:9000"
27 _OID = "sha256:" + "a" * 64
28
29
30 def _make_backend() -> BlobBackend:
31 """Backend wired with mismatched internal/public endpoints — mirrors local dev."""
32 return BlobBackend(
33 bucket="test-bucket",
34 endpoint_url=_INTERNAL,
35 public_endpoint_url=_PUBLIC,
36 access_key_id="minioadmin",
37 secret_access_key="minioadmin",
38 region="us-east-1",
39 )
40
41
42 def _fake_presigned(method: str) -> str:
43 """Simulated URL that boto3 would generate using the internal endpoint."""
44 return f"{_INTERNAL}/test-bucket/objects/{_OID}?X-Amz-SignedHeaders=host&Action={method}"
45
46
47 # ── T-A1 ──────────────────────────────────────────────────────────────────────
48
49 def test_presign_get_rewrites_internal_to_public_endpoint() -> None:
50 """presign_get must rewrite Docker-internal URL to the public endpoint.
51
52 RED before fix: presign_get returns http://minio:9000/... because it does
53 not call _rewrite_presign_url. External clients (muse CLI) cannot resolve
54 'minio' → [Errno 8] nodename nor servname provided, or not known.
55
56 GREEN after fix: backends.py:492 wraps the return with _rewrite_presign_url,
57 matching the pattern already used by presign_put and presign_batch.
58 """
59 backend = _make_backend()
60
61 mock_client = MagicMock()
62 mock_client.generate_presigned_url.return_value = _fake_presigned("GetObject")
63
64 with patch.object(backend, "_get_client", return_value=mock_client):
65 result = asyncio.run(backend.presign_get(_OID, ttl_seconds=3600))
66
67 assert result.startswith(_PUBLIC), (
68 f"presign_get must rewrite internal endpoint to public endpoint.\n"
69 f" internal ({_INTERNAL!r}) leaked into presigned URL: {result!r}\n"
70 f" expected URL to start with: {_PUBLIC!r}\n"
71 f"Fix: in backends.py presign_get, wrap the return value with\n"
72 f" self._rewrite_presign_url(...) — identical to presign_put."
73 )
74
75
76 # ── consistency ───────────────────────────────────────────────────────────────
77
78 def test_presign_put_already_rewrites() -> None:
79 """presign_put already rewrites correctly — serves as the passing baseline."""
80 backend = _make_backend()
81
82 mock_client = MagicMock()
83 mock_client.generate_presigned_url.return_value = _fake_presigned("PutObject")
84
85 with patch.object(backend, "_get_client", return_value=mock_client):
86 result = asyncio.run(backend.presign_put(_OID, ttl_seconds=3600))
87
88 assert result.startswith(_PUBLIC), (
89 f"presign_put baseline broken — expected {_PUBLIC!r}, got: {result!r}"
90 )
91
92
93 def test_presign_batch_get_already_rewrites() -> None:
94 """presign_batch (get direction) already rewrites — serves as baseline."""
95 backend = _make_backend()
96
97 mock_client = MagicMock()
98 mock_client.generate_presigned_url.return_value = _fake_presigned("GetObject")
99
100 with patch.object(backend, "_get_client", return_value=mock_client):
101 result = asyncio.run(backend.presign_batch([_OID], "get", 3600))
102
103 assert result[_OID].startswith(_PUBLIC), (
104 f"presign_batch baseline broken — expected {_PUBLIC!r}, got: {result[_OID]!r}"
105 )
106
107
108 def test_presign_get_no_rewrite_when_public_endpoint_unset() -> None:
109 """presign_get must not crash when public_endpoint_url is unset (production).
110
111 In prod, endpoint_url IS the public URL (R2 has one address), so no rewrite
112 is needed. _rewrite_presign_url is a no-op when _public_endpoint_url is None.
113 """
114 backend = BlobBackend(
115 bucket="test-bucket",
116 endpoint_url=_INTERNAL,
117 # public_endpoint_url intentionally omitted
118 access_key_id="minioadmin",
119 secret_access_key="minioadmin",
120 region="us-east-1",
121 )
122 assert backend._public_endpoint_url is None
123
124 mock_client = MagicMock()
125 mock_client.generate_presigned_url.return_value = _fake_presigned("GetObject")
126
127 with patch.object(backend, "_get_client", return_value=mock_client):
128 result = asyncio.run(backend.presign_get(_OID, ttl_seconds=3600))
129
130 # No rewrite — URL unchanged from what boto3 returned.
131 assert result == _fake_presigned("GetObject")
File History 1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor 1 day ago