gabriel / musehub public

test_request_signing_raw_path.py file-level

at main · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:9 Merge 'fix/assignee-sigil-inline' into 'dev' — proposal: Assignee sigil… · gabriel · Jun 7, 2026
1 """Regression tests for MSign raw-path extraction in _verify_msign.
2
3 Uvicorn sets scope["path"] to the *decoded* URL path — e.g. a DELETE to
4 /gabriel/muse/branches/feat%2Ffix-foo arrives in scope["path"] as
5 /gabriel/muse/branches/feat/fix-foo. The client signs the encoded form, so
6 using scope["path"] for the canonical message causes a signature mismatch and
7 an HTTP 401 for any branch name that contains a slash (feat/*, task/*).
8
9 The fix uses scope["raw_path"] (the encoded bytes off the wire) when available.
10 These tests lock down that behaviour.
11 """
12 from __future__ import annotations
13
14 from unittest.mock import MagicMock
15
16
17 # ---------------------------------------------------------------------------
18 # Helpers — replicate the path extraction logic from _verify_msign exactly
19 # ---------------------------------------------------------------------------
20
21 def _mock_request(raw_path: bytes, decoded_path: str, query: str = "") -> MagicMock:
22 req = MagicMock()
23 req.scope = {"raw_path": raw_path}
24 req.url.path = decoded_path
25 req.url.query = query
26 return req
27
28
29 def _extract_path(request: MagicMock) -> str:
30 """Mirror the path extraction block in musehub/auth/request_signing.py."""
31 _raw_path: bytes = request.scope.get("raw_path", b"")
32 path_with_query = _raw_path.decode("ascii", errors="replace") if _raw_path else request.url.path
33 if request.url.query:
34 path_with_query = f"{path_with_query}?{request.url.query}"
35 return path_with_query
36
37
38 # ---------------------------------------------------------------------------
39 # Tests
40 # ---------------------------------------------------------------------------
41
42 class TestMSignRawPathExtraction:
43
44 def test_encoded_slash_preserved_for_feat_branch(self) -> None:
45 """Core regression: feat/* branch delete must use encoded path."""
46 req = _mock_request(
47 raw_path=b"/gabriel/muse/branches/feat%2Ffix-agent-config-adapters",
48 decoded_path="/gabriel/muse/branches/feat/fix-agent-config-adapters",
49 )
50 path = _extract_path(req)
51 assert path == "/gabriel/muse/branches/feat%2Ffix-agent-config-adapters"
52
53 def test_encoded_slash_preserved_for_task_branch(self) -> None:
54 req = _mock_request(
55 raw_path=b"/gabriel/muse/branches/task%2Ffix-ops-commute",
56 decoded_path="/gabriel/muse/branches/task/fix-ops-commute",
57 )
58 path = _extract_path(req)
59 assert path == "/gabriel/muse/branches/task%2Ffix-ops-commute"
60
61 def test_decoded_form_absent_from_result(self) -> None:
62 """The decoded slash must never appear in the canonical path."""
63 req = _mock_request(
64 raw_path=b"/gabriel/muse/branches/feat%2Fmy-feature",
65 decoded_path="/gabriel/muse/branches/feat/my-feature",
66 )
67 path = _extract_path(req)
68 assert "feat/my-feature" not in path
69
70 def test_simple_path_without_encoding_unchanged(self) -> None:
71 """Paths with no percent-encoding pass through unchanged."""
72 req = _mock_request(
73 raw_path=b"/gabriel/muse/refs",
74 decoded_path="/gabriel/muse/refs",
75 )
76 assert _extract_path(req) == "/gabriel/muse/refs"
77
78 def test_fallback_to_url_path_when_raw_path_absent(self) -> None:
79 """ASGI servers that omit raw_path fall back to request.url.path."""
80 req = MagicMock()
81 req.scope = {}
82 req.url.path = "/gabriel/muse/refs"
83 req.url.query = ""
84 assert _extract_path(req) == "/gabriel/muse/refs"
85
86 def test_fallback_to_url_path_when_raw_path_empty(self) -> None:
87 """Empty raw_path bytes also trigger the fallback."""
88 req = _mock_request(raw_path=b"", decoded_path="/gabriel/muse/push/mpack-presign")
89 assert _extract_path(req) == "/gabriel/muse/push/mpack-presign"
90
91 def test_query_string_appended_with_encoded_path(self) -> None:
92 req = _mock_request(
93 raw_path=b"/search",
94 decoded_path="/search",
95 query="q=feat%2Fx",
96 )
97 assert _extract_path(req) == "/search?q=feat%2Fx"
98
99 def test_multiple_encoded_segments(self) -> None:
100 """Deeply nested encoded paths are preserved in full."""
101 req = _mock_request(
102 raw_path=b"/gabriel/muse/branches/feat%2Fauth%2Fv2",
103 decoded_path="/gabriel/muse/branches/feat/auth/v2",
104 )
105 assert _extract_path(req) == "/gabriel/muse/branches/feat%2Fauth%2Fv2"