"""TDD contract for BlobBackend presign URL rewriting. Bug (issue #62 — Phase 1): presign_get does not call _rewrite_presign_url, unlike presign_put and presign_batch. When BLOB_STORAGE_PUBLIC_ENDPOINT differs from BLOB_STORAGE_ENDPOINT (e.g. http://minio:9000 vs http://localhost:9000 in local dev), presign_get returns a URL with the Docker-internal hostname that external clients cannot reach. Root cause: backends.py:492 — return value is not wrapped in _rewrite_presign_url. Fix: wrap return value the same way presign_put (line 471) and presign_batch (line 517) already do. """ from __future__ import annotations import asyncio from unittest.mock import MagicMock, patch import pytest from musehub.storage.backends import BlobBackend _INTERNAL = "http://minio:9000" _PUBLIC = "http://localhost:9000" _OID = "sha256:" + "a" * 64 def _make_backend() -> BlobBackend: """Backend wired with mismatched internal/public endpoints — mirrors local dev.""" return BlobBackend( bucket="test-bucket", endpoint_url=_INTERNAL, public_endpoint_url=_PUBLIC, access_key_id="minioadmin", secret_access_key="minioadmin", region="us-east-1", ) def _fake_presigned(method: str) -> str: """Simulated URL that boto3 would generate using the internal endpoint.""" return f"{_INTERNAL}/test-bucket/objects/{_OID}?X-Amz-SignedHeaders=host&Action={method}" # ── T-A1 ────────────────────────────────────────────────────────────────────── def test_presign_get_rewrites_internal_to_public_endpoint() -> None: """presign_get must rewrite Docker-internal URL to the public endpoint. RED before fix: presign_get returns http://minio:9000/... because it does not call _rewrite_presign_url. External clients (muse CLI) cannot resolve 'minio' → [Errno 8] nodename nor servname provided, or not known. GREEN after fix: backends.py:492 wraps the return with _rewrite_presign_url, matching the pattern already used by presign_put and presign_batch. """ backend = _make_backend() mock_client = MagicMock() mock_client.generate_presigned_url.return_value = _fake_presigned("GetObject") with patch.object(backend, "_get_client", return_value=mock_client): result = asyncio.run(backend.presign_get(_OID, ttl_seconds=3600)) assert result.startswith(_PUBLIC), ( f"presign_get must rewrite internal endpoint to public endpoint.\n" f" internal ({_INTERNAL!r}) leaked into presigned URL: {result!r}\n" f" expected URL to start with: {_PUBLIC!r}\n" f"Fix: in backends.py presign_get, wrap the return value with\n" f" self._rewrite_presign_url(...) — identical to presign_put." ) # ── consistency ─────────────────────────────────────────────────────────────── def test_presign_put_already_rewrites() -> None: """presign_put already rewrites correctly — serves as the passing baseline.""" backend = _make_backend() mock_client = MagicMock() mock_client.generate_presigned_url.return_value = _fake_presigned("PutObject") with patch.object(backend, "_get_client", return_value=mock_client): result = asyncio.run(backend.presign_put(_OID, ttl_seconds=3600)) assert result.startswith(_PUBLIC), ( f"presign_put baseline broken — expected {_PUBLIC!r}, got: {result!r}" ) def test_presign_batch_get_already_rewrites() -> None: """presign_batch (get direction) already rewrites — serves as baseline.""" backend = _make_backend() mock_client = MagicMock() mock_client.generate_presigned_url.return_value = _fake_presigned("GetObject") with patch.object(backend, "_get_client", return_value=mock_client): result = asyncio.run(backend.presign_batch([_OID], "get", 3600)) assert result[_OID].startswith(_PUBLIC), ( f"presign_batch baseline broken — expected {_PUBLIC!r}, got: {result[_OID]!r}" ) def test_presign_get_no_rewrite_when_public_endpoint_unset() -> None: """presign_get must not crash when public_endpoint_url is unset (production). In prod, endpoint_url IS the public URL (R2 has one address), so no rewrite is needed. _rewrite_presign_url is a no-op when _public_endpoint_url is None. """ backend = BlobBackend( bucket="test-bucket", endpoint_url=_INTERNAL, # public_endpoint_url intentionally omitted access_key_id="minioadmin", secret_access_key="minioadmin", region="us-east-1", ) assert backend._public_endpoint_url is None mock_client = MagicMock() mock_client.generate_presigned_url.return_value = _fake_presigned("GetObject") with patch.object(backend, "_get_client", return_value=mock_client): result = asyncio.run(backend.presign_get(_OID, ttl_seconds=3600)) # No rewrite — URL unchanged from what boto3 returned. assert result == _fake_presigned("GetObject")