"""Regression tests for MSign raw-path extraction in _verify_msign. Uvicorn sets scope["path"] to the *decoded* URL path — e.g. a DELETE to /gabriel/muse/branches/feat%2Ffix-foo arrives in scope["path"] as /gabriel/muse/branches/feat/fix-foo. The client signs the encoded form, so using scope["path"] for the canonical message causes a signature mismatch and an HTTP 401 for any branch name that contains a slash (feat/*, task/*). The fix uses scope["raw_path"] (the encoded bytes off the wire) when available. These tests lock down that behaviour. """ from __future__ import annotations from unittest.mock import MagicMock # --------------------------------------------------------------------------- # Helpers — replicate the path extraction logic from _verify_msign exactly # --------------------------------------------------------------------------- def _mock_request(raw_path: bytes, decoded_path: str, query: str = "") -> MagicMock: req = MagicMock() req.scope = {"raw_path": raw_path} req.url.path = decoded_path req.url.query = query return req def _extract_path(request: MagicMock) -> str: """Mirror the path extraction block in musehub/auth/request_signing.py.""" _raw_path: bytes = request.scope.get("raw_path", b"") path_with_query = _raw_path.decode("ascii", errors="replace") if _raw_path else request.url.path if request.url.query: path_with_query = f"{path_with_query}?{request.url.query}" return path_with_query # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- class TestMSignRawPathExtraction: def test_encoded_slash_preserved_for_feat_branch(self) -> None: """Core regression: feat/* branch delete must use encoded path.""" req = _mock_request( raw_path=b"/gabriel/muse/branches/feat%2Ffix-agent-config-adapters", decoded_path="/gabriel/muse/branches/feat/fix-agent-config-adapters", ) path = _extract_path(req) assert path == "/gabriel/muse/branches/feat%2Ffix-agent-config-adapters" def test_encoded_slash_preserved_for_task_branch(self) -> None: req = _mock_request( raw_path=b"/gabriel/muse/branches/task%2Ffix-ops-commute", decoded_path="/gabriel/muse/branches/task/fix-ops-commute", ) path = _extract_path(req) assert path == "/gabriel/muse/branches/task%2Ffix-ops-commute" def test_decoded_form_absent_from_result(self) -> None: """The decoded slash must never appear in the canonical path.""" req = _mock_request( raw_path=b"/gabriel/muse/branches/feat%2Fmy-feature", decoded_path="/gabriel/muse/branches/feat/my-feature", ) path = _extract_path(req) assert "feat/my-feature" not in path def test_simple_path_without_encoding_unchanged(self) -> None: """Paths with no percent-encoding pass through unchanged.""" req = _mock_request( raw_path=b"/gabriel/muse/refs", decoded_path="/gabriel/muse/refs", ) assert _extract_path(req) == "/gabriel/muse/refs" def test_fallback_to_url_path_when_raw_path_absent(self) -> None: """ASGI servers that omit raw_path fall back to request.url.path.""" req = MagicMock() req.scope = {} req.url.path = "/gabriel/muse/refs" req.url.query = "" assert _extract_path(req) == "/gabriel/muse/refs" def test_fallback_to_url_path_when_raw_path_empty(self) -> None: """Empty raw_path bytes also trigger the fallback.""" req = _mock_request(raw_path=b"", decoded_path="/gabriel/muse/push/mpack-presign") assert _extract_path(req) == "/gabriel/muse/push/mpack-presign" def test_query_string_appended_with_encoded_path(self) -> None: req = _mock_request( raw_path=b"/search", decoded_path="/search", query="q=feat%2Fx", ) assert _extract_path(req) == "/search?q=feat%2Fx" def test_multiple_encoded_segments(self) -> None: """Deeply nested encoded paths are preserved in full.""" req = _mock_request( raw_path=b"/gabriel/muse/branches/feat%2Fauth%2Fv2", decoded_path="/gabriel/muse/branches/feat/auth/v2", ) assert _extract_path(req) == "/gabriel/muse/branches/feat%2Fauth%2Fv2"