gabriel / musehub public
test_bot_throttle.py python
190 lines 6.7 KB
Raw
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago
1 """Tests for BotThrottleMiddleware — agent-first principal model.
2
3 The core invariant: MSign-authenticated requests bypass UA checks entirely.
4 UA-based blocking applies only to unauthenticated requests.
5 """
6 from __future__ import annotations
7
8 import pytest
9 from httpx import AsyncClient
10
11
12 # ---------------------------------------------------------------------------
13 # Authenticated requests — always pass through regardless of UA
14 # ---------------------------------------------------------------------------
15
16 @pytest.mark.anyio
17 async def test_msign_with_curl_ua_passes(client: AsyncClient) -> None:
18 """MSign-authenticated curl calls must not be blocked.
19
20 Developers and agents routinely test with curl. Once they sign the
21 request, the UA is irrelevant — identity is proven cryptographically.
22 """
23 resp = await client.get(
24 "/healthz",
25 headers={
26 "User-Agent": "curl/8.4.0",
27 "Authorization": "MSign handle=\"gabriel\" ts=1234567890 sig=\"fakesig\"",
28 },
29 )
30 # Must not be 429 — may be 401/403 (invalid sig) but never bot-blocked
31 assert resp.status_code != 429
32
33
34 @pytest.mark.anyio
35 async def test_msign_with_python_requests_ua_passes(client: AsyncClient) -> None:
36 """MSign-authenticated python-requests calls must not be blocked."""
37 resp = await client.get(
38 "/healthz",
39 headers={
40 "User-Agent": "python-requests/2.31.0",
41 "Authorization": "MSign handle=\"agentception-abc123\" ts=1234567890 sig=\"fakesig\"",
42 },
43 )
44 assert resp.status_code != 429
45
46
47 @pytest.mark.anyio
48 async def test_msign_with_go_ua_passes(client: AsyncClient) -> None:
49 """MSign-authenticated Go HTTP client calls must not be blocked."""
50 resp = await client.get(
51 "/healthz",
52 headers={
53 "User-Agent": "Go-http-client/1.1",
54 "Authorization": "MSign handle=\"some-agent\" ts=1234567890 sig=\"fakesig\"",
55 },
56 )
57 assert resp.status_code != 429
58
59
60 @pytest.mark.anyio
61 async def test_msign_with_no_ua_passes(client: AsyncClient) -> None:
62 """MSign-authenticated requests with no UA must not be blocked.
63
64 An agent that omits the UA header entirely is still authenticated.
65 """
66 resp = await client.get(
67 "/healthz",
68 headers={
69 "Authorization": "MSign handle=\"agent-42\" ts=1234567890 sig=\"fakesig\"",
70 },
71 )
72 assert resp.status_code != 429
73
74
75 # ---------------------------------------------------------------------------
76 # Unauthenticated requests with bad UAs — must be blocked
77 # ---------------------------------------------------------------------------
78
79 @pytest.mark.anyio
80 async def test_unauthenticated_curl_ua_blocked(client: AsyncClient) -> None:
81 """Unauthenticated curl requests are blocked — commodity scraper signal."""
82 resp = await client.get(
83 "/api/identities",
84 headers={"User-Agent": "curl/8.4.0"},
85 )
86 assert resp.status_code == 429
87
88
89 @pytest.mark.anyio
90 async def test_unauthenticated_python_requests_ua_blocked(client: AsyncClient) -> None:
91 """Unauthenticated python-requests are blocked."""
92 resp = await client.get(
93 "/api/identities",
94 headers={"User-Agent": "python-requests/2.31.0"},
95 )
96 assert resp.status_code == 429
97
98
99 @pytest.mark.anyio
100 async def test_unauthenticated_go_ua_blocked(client: AsyncClient) -> None:
101 """Unauthenticated Go HTTP client requests are blocked."""
102 resp = await client.get(
103 "/api/identities",
104 headers={"User-Agent": "Go-http-client/1.1"},
105 )
106 assert resp.status_code == 429
107
108
109 @pytest.mark.anyio
110 async def test_unauthenticated_missing_ua_blocked(client: AsyncClient) -> None:
111 """Unauthenticated requests with an empty UA are blocked.
112
113 httpx always sends a UA, so we explicitly blank it to simulate a
114 client that omits the header.
115 """
116 resp = await client.get("/api/identities", headers={"User-Agent": ""})
117 assert resp.status_code == 429
118
119
120 @pytest.mark.anyio
121 async def test_unauthenticated_scanner_ua_blocked(client: AsyncClient) -> None:
122 """Vulnerability scanners are blocked regardless of auth header absence."""
123 for ua in ["sqlmap/1.7", "nikto/2.1.6", "nuclei/3.0", "masscan/1.3"]:
124 resp = await client.get(
125 "/api/identities",
126 headers={"User-Agent": ua},
127 )
128 assert resp.status_code == 429, f"Expected 429 for UA: {ua}"
129
130
131 # ---------------------------------------------------------------------------
132 # Exempt paths — always pass through
133 # ---------------------------------------------------------------------------
134
135 @pytest.mark.anyio
136 async def test_healthz_passes_with_no_ua(client: AsyncClient) -> None:
137 """/healthz must never be blocked — monitoring probes have minimal UAs."""
138 resp = await client.get("/healthz")
139 assert resp.status_code != 429
140
141
142 @pytest.mark.anyio
143 async def test_healthz_passes_with_curl_ua(client: AsyncClient) -> None:
144 """/healthz must pass even with a normally-blocked UA."""
145 resp = await client.get("/healthz", headers={"User-Agent": "curl/8.4.0"})
146 assert resp.status_code != 429
147
148
149 # ---------------------------------------------------------------------------
150 # Error message — must be informative, not accusatory
151 # ---------------------------------------------------------------------------
152
153 @pytest.mark.anyio
154 async def test_blocked_response_body_is_informative(client: AsyncClient) -> None:
155 """Blocked response must guide the client toward authentication."""
156 resp = await client.get(
157 "/api/identities",
158 headers={"User-Agent": "curl/8.4.0"},
159 )
160 assert resp.status_code == 429
161 body = resp.json()
162 assert "MSign" in body["detail"] or "authenticate" in body["detail"].lower()
163
164
165 # ---------------------------------------------------------------------------
166 # Muse CLI UA — always passes unauthenticated (it's a known good client)
167 # ---------------------------------------------------------------------------
168
169 @pytest.mark.anyio
170 async def test_muse_cli_ua_passes_unauthenticated(client: AsyncClient) -> None:
171 """The muse CLI UA must not be blocked even without auth.
172
173 muse CLI sends 'muse/<version>' for unauthenticated pre-flight calls
174 like listing remotes before signing in.
175 """
176 resp = await client.get(
177 "/api/identities",
178 headers={"User-Agent": "muse/1.0.0"},
179 )
180 assert resp.status_code != 429
181
182
183 @pytest.mark.anyio
184 async def test_browser_ua_passes_unauthenticated(client: AsyncClient) -> None:
185 """Standard browser UAs must pass without auth."""
186 resp = await client.get(
187 "/api/identities",
188 headers={"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"},
189 )
190 assert resp.status_code != 429
File History 1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago