test_storage_backends.py
python
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