gabriel / musehub public
test_install_script_section43.py python
525 lines 22.6 KB
Raw
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 170 days ago
1 """Section 43 — Install Script: 7-layer test suite.
2
3 Covers:
4 - musehub/api/routes/musehub/install.py::sanitize_musehub_url,
5 build_archive_url, generate_install_script, generate_uninstall_script,
6 GET /install.sh, GET /uninstall.sh
7 """
8 from __future__ import annotations
9
10 import re
11 import time
12
13 import pytest
14
15 from musehub.api.routes.musehub.install import (
16 _SAFE_URL_RE,
17 build_archive_url,
18 generate_install_script,
19 generate_uninstall_script,
20 sanitize_musehub_url,
21 )
22 from musehub.protocol.version import MUSE_VERSION
23
24
25 # ─────────────────────────────────────────────────────────────────────────────
26 # LAYER 1 — UNIT
27 # ─────────────────────────────────────────────────────────────────────────────
28
29
30 class TestSanitizeMusehubUrlUnit:
31 """Unit: sanitize_musehub_url rejects unsafe URLs."""
32
33 def test_accepts_http_url(self) -> None:
34 assert sanitize_musehub_url("http://localhost:10003") == "http://localhost:10003"
35
36 def test_accepts_https_url(self) -> None:
37 assert sanitize_musehub_url("https://musehub.example.com") == "https://musehub.example.com"
38
39 def test_accepts_url_with_path(self) -> None:
40 result = sanitize_musehub_url("https://hub.example.com/muse")
41 assert result == "https://hub.example.com/muse"
42
43 def test_strips_trailing_slash(self) -> None:
44 result = sanitize_musehub_url("https://hub.example.com/")
45 assert not result.endswith("/")
46
47 def test_rejects_url_with_double_quote(self) -> None:
48 with pytest.raises(ValueError):
49 sanitize_musehub_url('https://example.com"evil')
50
51 def test_rejects_url_with_single_quote(self) -> None:
52 with pytest.raises(ValueError):
53 sanitize_musehub_url("https://example.com'evil")
54
55 def test_rejects_url_with_backtick(self) -> None:
56 with pytest.raises(ValueError):
57 sanitize_musehub_url("https://example.com`id`")
58
59 def test_rejects_url_with_dollar(self) -> None:
60 with pytest.raises(ValueError):
61 sanitize_musehub_url("https://example.com$HOME")
62
63 def test_rejects_url_with_semicolon(self) -> None:
64 with pytest.raises(ValueError):
65 sanitize_musehub_url("https://example.com;rm -rf /")
66
67 def test_rejects_url_with_newline(self) -> None:
68 with pytest.raises(ValueError):
69 sanitize_musehub_url("https://example.com\nevil")
70
71 def test_rejects_non_http_scheme(self) -> None:
72 with pytest.raises(ValueError):
73 sanitize_musehub_url("ftp://example.com")
74
75 def test_rejects_empty_string(self) -> None:
76 with pytest.raises(ValueError):
77 sanitize_musehub_url("")
78
79
80 class TestBuildArchiveUrlUnit:
81 """Unit: build_archive_url produces correct URL strings."""
82
83 def test_linux_x86_64(self) -> None:
84 url = build_archive_url("https://hub.example.com", "1.2.3", "linux", "x86_64")
85 assert url == "https://hub.example.com/releases/muse-1.2.3-linux-x86_64.tar.gz"
86
87 def test_darwin_arm64(self) -> None:
88 url = build_archive_url("https://hub.example.com", "1.2.3", "darwin", "arm64")
89 assert url == "https://hub.example.com/releases/muse-1.2.3-darwin-arm64.tar.gz"
90
91 def test_url_ends_with_tar_gz(self) -> None:
92 url = build_archive_url("http://localhost:10003", "0.1.0", "linux", "x86_64")
93 assert url.endswith(".tar.gz")
94
95 def test_version_embedded_in_url(self) -> None:
96 url = build_archive_url("http://localhost", "9.9.9", "linux", "x86_64")
97 assert "9.9.9" in url
98
99 def test_platform_embedded_in_url(self) -> None:
100 url = build_archive_url("http://localhost", "1.0.0", "darwin", "x86_64")
101 assert "darwin" in url
102
103 def test_arch_embedded_in_url(self) -> None:
104 url = build_archive_url("http://localhost", "1.0.0", "linux", "arm64")
105 assert "arm64" in url
106
107
108 class TestGenerateInstallScriptUnit:
109 """Unit: generate_install_script content and structure."""
110
111 def test_starts_with_shebang(self) -> None:
112 script = generate_install_script("http://localhost:10003")
113 assert script.startswith("#!/usr/bin/env sh")
114
115 def test_contains_musehub_url(self) -> None:
116 url = "https://hub.example.com"
117 script = generate_install_script(url)
118 assert url in script
119
120 def test_url_is_double_quoted_in_assignment(self) -> None:
121 url = "https://hub.example.com"
122 script = generate_install_script(url)
123 assert f'MUSEHUB_URL="{url}"' in script
124
125 def test_contains_muse_version(self) -> None:
126 script = generate_install_script("http://localhost:10003")
127 assert MUSE_VERSION in script
128
129 def test_contains_set_euf(self) -> None:
130 script = generate_install_script("http://localhost:10003")
131 assert "set -euf" in script
132
133 def test_contains_tar_gz_archive_reference(self) -> None:
134 script = generate_install_script("http://localhost:10003")
135 assert ".tar.gz" in script
136
137 def test_contains_chmod_plus_x(self) -> None:
138 script = generate_install_script("http://localhost:10003")
139 assert "chmod +x" in script
140
141 def test_no_eval_in_script(self) -> None:
142 script = generate_install_script("http://localhost:10003")
143 assert "\neval " not in script and " eval " not in script
144
145 def test_raises_for_unsafe_url(self) -> None:
146 with pytest.raises(ValueError):
147 generate_install_script('http://evil.com";rm -rf /')
148
149 def test_custom_version_embedded(self) -> None:
150 script = generate_install_script("http://localhost:10003", version="99.0.0")
151 assert "99.0.0" in script
152
153
154 class TestGenerateUninstallScriptUnit:
155 """Unit: generate_uninstall_script never removes ~/.muse data."""
156
157 def test_starts_with_shebang(self) -> None:
158 script = generate_uninstall_script()
159 assert script.startswith("#!/usr/bin/env sh")
160
161 def test_contains_set_euf(self) -> None:
162 script = generate_uninstall_script()
163 assert "set -euf" in script
164
165 def test_mentions_muse_dir_preserved(self) -> None:
166 script = generate_uninstall_script()
167 assert "~/.muse" in script
168 assert "preserved" in script.lower() or "never" in script.lower()
169
170 def test_does_not_rm_rf_muse_dir(self) -> None:
171 script = generate_uninstall_script()
172 # Must NOT have a line where rm is the actual command targeting .muse.
173 # A printf/echo mentioning "rm -rf ~/.muse" as a hint to the user is fine.
174 for line in script.splitlines():
175 stripped = line.strip()
176 if stripped.startswith("#"):
177 continue # comments are fine
178 # Only flag lines where rm is the leading command (not inside a string)
179 if re.match(r"^rm\s", stripped) and ".muse" in stripped:
180 pytest.fail(f"Active rm targeting .muse: {stripped!r}")
181
182 def test_removes_only_binary(self) -> None:
183 script = generate_uninstall_script()
184 assert "rm" in script # removes the binary
185 assert "muse" in script # references the muse binary
186
187
188 # ─────────────────────────────────────────────────────────────────────────────
189 # LAYER 2 — INTEGRATION
190 # ─────────────────────────────────────────────────────────────────────────────
191
192
193 class TestInstallScriptIntegration:
194 """Integration: script functions work together coherently."""
195
196 def test_install_script_archive_url_is_reachable_pattern(self) -> None:
197 """The archive URL in the install script follows the build_archive_url pattern."""
198 base = "https://hub.example.com"
199 script = generate_install_script(base)
200 # The script constructs the URL dynamically; the base URL should be embedded
201 assert base in script
202
203 def test_build_archive_url_matches_all_supported_combinations(self) -> None:
204 combos = [
205 ("linux", "x86_64"),
206 ("linux", "arm64"),
207 ("darwin", "x86_64"),
208 ("darwin", "arm64"),
209 ]
210 for platform, arch in combos:
211 url = build_archive_url("https://hub.example.com", MUSE_VERSION, platform, arch)
212 assert MUSE_VERSION in url
213 assert platform in url
214 assert arch in url
215 assert url.endswith(".tar.gz")
216
217 def test_safe_url_re_pattern_matches_valid_urls(self) -> None:
218 valid = [
219 "http://localhost:10003",
220 "https://musehub.example.com",
221 "https://hub.example.com/muse",
222 "http://192.168.1.1:8080",
223 ]
224 for url in valid:
225 assert _SAFE_URL_RE.match(url), f"Expected {url!r} to be safe"
226
227 def test_safe_url_re_pattern_rejects_metacharacters(self) -> None:
228 dangerous = [
229 "http://evil.com\";rm -rf /",
230 "http://evil.com`id`",
231 "http://evil.com$HOME",
232 "http://evil.com;evil",
233 "http://evil.com'x",
234 ]
235 for url in dangerous:
236 assert not _SAFE_URL_RE.match(url), f"Expected {url!r} to be rejected"
237
238 def test_generate_install_script_version_matches_muse_version_default(self) -> None:
239 script = generate_install_script("http://localhost")
240 assert MUSE_VERSION in script
241
242 def test_uninstall_does_not_reference_musehub_url(self) -> None:
243 """Uninstall script is URL-independent — no MUSEHUB_URL substitution needed."""
244 script = generate_uninstall_script()
245 assert "MUSEHUB_URL" not in script
246
247
248 # ─────────────────────────────────────────────────────────────────────────────
249 # LAYER 3 — E2E
250 # ─────────────────────────────────────────────────────────────────────────────
251
252
253 class TestInstallScriptE2E:
254 """E2E: /install.sh and /uninstall.sh endpoints via async test client."""
255
256 async def test_get_install_sh_returns_200(self, client) -> None:
257 r = await client.get("/install.sh")
258 assert r.status_code == 200
259
260 async def test_get_install_sh_content_type_is_shell(self, client) -> None:
261 r = await client.get("/install.sh")
262 ct = r.headers.get("content-type", "")
263 assert "sh" in ct or "plain" in ct
264
265 async def test_get_install_sh_starts_with_shebang(self, client) -> None:
266 r = await client.get("/install.sh")
267 assert r.text.startswith("#!/usr/bin/env sh")
268
269 async def test_get_install_sh_contains_set_euf(self, client) -> None:
270 r = await client.get("/install.sh")
271 assert "set -euf" in r.text
272
273 async def test_get_install_sh_contains_muse_version(self, client) -> None:
274 r = await client.get("/install.sh")
275 assert MUSE_VERSION in r.text
276
277 async def test_get_install_sh_contains_tar_gz(self, client) -> None:
278 r = await client.get("/install.sh")
279 assert ".tar.gz" in r.text
280
281 async def test_get_install_sh_no_auth_required(self, client) -> None:
282 r = await client.get("/install.sh")
283 assert r.status_code == 200
284
285 async def test_get_uninstall_sh_returns_200(self, client) -> None:
286 r = await client.get("/uninstall.sh")
287 assert r.status_code == 200
288
289 async def test_get_uninstall_sh_content_type_is_shell(self, client) -> None:
290 r = await client.get("/uninstall.sh")
291 ct = r.headers.get("content-type", "")
292 assert "sh" in ct or "plain" in ct
293
294 async def test_get_uninstall_sh_starts_with_shebang(self, client) -> None:
295 r = await client.get("/uninstall.sh")
296 assert r.text.startswith("#!/usr/bin/env sh")
297
298 async def test_get_uninstall_sh_no_auth_required(self, client) -> None:
299 r = await client.get("/uninstall.sh")
300 assert r.status_code == 200
301
302 async def test_get_uninstall_sh_does_not_rm_muse_dir(self, client) -> None:
303 r = await client.get("/uninstall.sh")
304 for line in r.text.splitlines():
305 stripped = line.strip()
306 if stripped.startswith("#"):
307 continue
308 if re.match(r"^rm\s", stripped) and ".muse" in stripped:
309 pytest.fail(f"Uninstall script removes .muse: {stripped!r}")
310
311 async def test_get_uninstall_sh_mentions_muse_dir_preserved(self, client) -> None:
312 r = await client.get("/uninstall.sh")
313 text = r.text.lower()
314 assert "~/.muse" in text
315 assert "preserved" in text or "never" in text
316
317
318 # ─────────────────────────────────────────────────────────────────────────────
319 # LAYER 4 — STRESS
320 # ─────────────────────────────────────────────────────────────────────────────
321
322
323 class TestInstallScriptStress:
324 """Stress: repeated generation and URL validation at volume."""
325
326 def test_generate_install_script_1000_times_identical(self) -> None:
327 url = "https://hub.example.com"
328 first = generate_install_script(url)
329 for _ in range(1000):
330 assert generate_install_script(url) == first
331
332 def test_generate_uninstall_script_1000_times_identical(self) -> None:
333 first = generate_uninstall_script()
334 for _ in range(1000):
335 assert generate_uninstall_script() == first
336
337 def test_sanitize_url_10000_valid_urls(self) -> None:
338 for i in range(10_000):
339 url = f"https://hub{i}.example.com"
340 result = sanitize_musehub_url(url)
341 assert result == url
342
343 def test_build_archive_url_1000_times(self) -> None:
344 for _ in range(1000):
345 url = build_archive_url("https://hub.example.com", MUSE_VERSION, "linux", "x86_64")
346 assert url.endswith(".tar.gz")
347
348 async def test_get_install_sh_20_sequential_requests_identical(self, client) -> None:
349 first = None
350 for _ in range(20):
351 r = await client.get("/install.sh")
352 assert r.status_code == 200
353 if first is None:
354 first = r.text
355 assert r.text == first
356
357
358 # ─────────────────────────────────────────────────────────────────────────────
359 # LAYER 5 — DATA INTEGRITY
360 # ─────────────────────────────────────────────────────────────────────────────
361
362
363 class TestInstallScriptDataIntegrity:
364 """Data Integrity: script content correctness and determinism."""
365
366 def test_install_script_is_valid_utf8(self) -> None:
367 script = generate_install_script("http://localhost:10003")
368 encoded = script.encode("utf-8")
369 assert encoded.decode("utf-8") == script
370
371 def test_uninstall_script_is_valid_utf8(self) -> None:
372 script = generate_uninstall_script()
373 encoded = script.encode("utf-8")
374 assert encoded.decode("utf-8") == script
375
376 def test_install_script_deterministic_same_url(self) -> None:
377 url = "https://hub.example.com"
378 assert generate_install_script(url) == generate_install_script(url)
379
380 def test_install_script_different_for_different_urls(self) -> None:
381 s1 = generate_install_script("https://hub1.example.com")
382 s2 = generate_install_script("https://hub2.example.com")
383 assert s1 != s2
384
385 def test_archive_url_unique_per_platform(self) -> None:
386 combos = [("linux", "x86_64"), ("linux", "arm64"), ("darwin", "x86_64"), ("darwin", "arm64")]
387 urls = [build_archive_url("https://hub.example.com", "1.0.0", p, a) for p, a in combos]
388 assert len(urls) == len(set(urls))
389
390 def test_uninstall_script_does_not_change_between_calls(self) -> None:
391 a = generate_uninstall_script()
392 b = generate_uninstall_script()
393 assert a == b
394
395 def test_install_script_uses_mktemp_for_tmp_dir(self) -> None:
396 script = generate_install_script("http://localhost")
397 assert "mktemp" in script
398
399 def test_install_script_cleans_up_tmp_on_exit(self) -> None:
400 script = generate_install_script("http://localhost")
401 # Must use trap or explicit cleanup — not just rm at end
402 assert "trap" in script or "cleanup" in script
403
404 def test_install_script_creates_install_dir_with_mkdir_p(self) -> None:
405 script = generate_install_script("http://localhost")
406 assert "mkdir -p" in script
407
408
409 # ─────────────────────────────────────────────────────────────────────────────
410 # LAYER 6 — SECURITY
411 # ─────────────────────────────────────────────────────────────────────────────
412
413
414 class TestInstallScriptSecurity:
415 """Security: shell injection prevention and uninstall data-preservation guarantee."""
416
417 @pytest.mark.parametrize("injection", [
418 'https://evil.com"; rm -rf / ; echo "',
419 "https://evil.com`id`",
420 "https://evil.com$(id)",
421 "https://evil.com$HOME",
422 "https://evil.com; cat /etc/passwd",
423 "https://evil.com\nrm -rf /",
424 "https://evil.com' && evil",
425 ])
426 def test_sanitize_rejects_injection_url(self, injection: str) -> None:
427 with pytest.raises(ValueError):
428 sanitize_musehub_url(injection)
429
430 @pytest.mark.parametrize("injection", [
431 'https://evil.com"; rm -rf / ; echo "',
432 "https://evil.com`id`",
433 ])
434 def test_generate_install_raises_for_injection(self, injection: str) -> None:
435 with pytest.raises(ValueError):
436 generate_install_script(injection)
437
438 def test_uninstall_script_never_removes_muse_dir(self) -> None:
439 script = generate_uninstall_script()
440 # Check all non-comment lines for rm as the leading command targeting .muse.
441 # Informational printf/echo lines that mention rm are acceptable.
442 for line in script.splitlines():
443 stripped = line.strip()
444 if stripped.startswith("#"):
445 continue
446 if re.match(r"^rm\s", stripped) and ".muse" in stripped:
447 pytest.fail(f"Active rm targeting .muse: {stripped!r}")
448
449 def test_install_script_no_eval(self) -> None:
450 script = generate_install_script("http://localhost")
451 # No unconditional eval (allow in comments)
452 for line in script.splitlines():
453 if line.strip().startswith("#"):
454 continue
455 assert not re.match(r"^\s*eval\s", line), f"eval found: {line!r}"
456
457 def test_install_script_no_source_untrusted(self) -> None:
458 script = generate_install_script("http://localhost")
459 for line in script.splitlines():
460 if line.strip().startswith("#"):
461 continue
462 # source or dot-source of a URL is dangerous
463 assert not re.search(r"\bsource\s+https?://", line)
464 assert not re.search(r"^\s*\.\s+https?://", line)
465
466 async def test_endpoints_return_200_not_500(self, client) -> None:
467 for path in ("/install.sh", "/uninstall.sh"):
468 r = await client.get(path)
469 assert r.status_code == 200, f"{path} returned {r.status_code}"
470
471 async def test_endpoints_no_stack_trace(self, client) -> None:
472 for path in ("/install.sh", "/uninstall.sh"):
473 r = await client.get(path)
474 assert "Traceback" not in r.text
475
476 def test_install_script_musehub_url_quoted(self) -> None:
477 url = "https://hub.example.com"
478 script = generate_install_script(url)
479 # The MUSEHUB_URL assignment must be double-quoted
480 assert f'MUSEHUB_URL="{url}"' in script
481
482
483 # ─────────────────────────────────────────────────────────────────────────────
484 # LAYER 7 — PERFORMANCE
485 # ─────────────────────────────────────────────────────────────────────────────
486
487
488 class TestInstallScriptPerformance:
489 """Performance: script generation and endpoint latency budgets."""
490
491 def test_generate_install_script_under_5ms(self) -> None:
492 url = "http://localhost:10003"
493 t0 = time.perf_counter()
494 generate_install_script(url)
495 elapsed = time.perf_counter() - t0
496 assert elapsed < 0.005, f"generate_install_script took {elapsed*1000:.1f}ms"
497
498 def test_generate_uninstall_script_under_1ms(self) -> None:
499 t0 = time.perf_counter()
500 generate_uninstall_script()
501 elapsed = time.perf_counter() - t0
502 assert elapsed < 0.001, f"generate_uninstall_script took {elapsed*1000:.1f}ms"
503
504 def test_sanitize_url_under_1ms_per_call(self) -> None:
505 url = "https://hub.example.com"
506 t0 = time.perf_counter()
507 for _ in range(1000):
508 sanitize_musehub_url(url)
509 elapsed = time.perf_counter() - t0
510 # 1000 calls should finish well under 100ms total
511 assert elapsed < 0.1, f"1K sanitize_url calls took {elapsed*1000:.1f}ms"
512
513 async def test_get_install_sh_under_100ms(self, client) -> None:
514 t0 = time.perf_counter()
515 r = await client.get("/install.sh")
516 elapsed = time.perf_counter() - t0
517 assert r.status_code == 200
518 assert elapsed < 0.1, f"GET /install.sh took {elapsed*1000:.1f}ms"
519
520 async def test_get_uninstall_sh_under_100ms(self, client) -> None:
521 t0 = time.perf_counter()
522 r = await client.get("/uninstall.sh")
523 elapsed = time.perf_counter() - t0
524 assert r.status_code == 200
525 assert elapsed < 0.1, f"GET /uninstall.sh took {elapsed*1000:.1f}ms"
File History 1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 170 days ago