test_branch_validation.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. #!/usr/bin/env python3
  2. """Exercise real download functions with deterministic HTTP responses; no network required."""
  3. import os
  4. import re
  5. import subprocess
  6. import tempfile
  7. import unittest
  8. from pathlib import Path
  9. ROOT = Path(__file__).resolve().parents[1]
  10. def functions(path):
  11. """Load Bash definitions without executing a launcher or module."""
  12. return "\n".join(
  13. re.findall(r"(?ms)^[\w.]+\(\) \{\n.*?^\}", path.read_text(encoding="utf-8"))
  14. )
  15. BOOTSTRAP = functions(ROOT / "linuxgsm.sh")
  16. DOWNLOAD = functions(ROOT / "lgsm/modules/core_dl.sh")
  17. UPDATE = (
  18. (ROOT / "lgsm/modules/command_update_linuxgsm.sh")
  19. .read_text()
  20. .split("# Check linuxsm.sh", 1)[0]
  21. )
  22. MOCKS = r"""
  23. githubuser="GameServerManagers"
  24. githubrepo="LinuxGSM"
  25. githubbranch="${TEST_BRANCH:-feature/example}"
  26. version="v26.2.0"
  27. rootdir="$TEST_DIR"
  28. local_filedir="$TEST_DIR"
  29. modulesdir="$TEST_DIR"
  30. lgsmlog="$TEST_DIR/log"
  31. fn_firstcommand_set() { :; }
  32. fn_print_dots() { :; }
  33. fn_script_log_info() { :; }
  34. fn_script_log_fail() { printf '%s\n' "$*" >> "$TEST_DIR/log"; }
  35. fn_print_fail_nl() { printf '%s\n' "$*"; }
  36. fn_print_ok_nl() { :; }
  37. check.sh() { :; }
  38. info_distro.sh() { :; }
  39. info_game.sh() { :; }
  40. curl() {
  41. printf '%s\n' "$*" >> "$TEST_DIR/calls"
  42. local output="" probe="" url="${!#}"
  43. while [ "$#" -gt 0 ]; do
  44. case "$1" in
  45. --write-out) probe=1; shift ;;
  46. -o) output="$2"; shift ;;
  47. esac
  48. shift
  49. done
  50. if [ "$probe" ]; then
  51. printf '%s' "${HTTP_CODE:-200}"
  52. return "${PROBE_EXIT:-0}"
  53. fi
  54. if [[ "$url" == https://raw.githubusercontent.com/* ]] && [ "${PRIMARY_FAIL:-0}" == 1 ]; then
  55. return 22
  56. fi
  57. if [ "$output" ]; then
  58. printf '#!/bin/bash\n' > "$output"
  59. fi
  60. }
  61. """
  62. class BranchValidation(unittest.TestCase):
  63. """Run production Bash with controlled HTTP responses and isolated files."""
  64. def run_bash(self, code, *, core=False, legacy=False, **env):
  65. """Return process results, HTTP calls, log output and download existence."""
  66. with tempfile.TemporaryDirectory() as directory:
  67. source = BOOTSTRAP + ("\n" + DOWNLOAD if core else "")
  68. if legacy:
  69. source += "\nunset -f fn_check_github_branch\n"
  70. result = subprocess.run(
  71. ["bash", "-c", source + "\n" + MOCKS + "\n" + code],
  72. env={**os.environ, "TEST_DIR": directory, **env},
  73. check=False,
  74. capture_output=True,
  75. text=True,
  76. timeout=10,
  77. )
  78. calls = Path(directory, "calls")
  79. log = Path(directory, "log")
  80. return (
  81. result,
  82. (
  83. calls.read_text(encoding="utf-8").splitlines()
  84. if calls.exists()
  85. else []
  86. ),
  87. log.read_text(encoding="utf-8") if log.exists() else "",
  88. Path(directory, "sample.sh").exists(),
  89. )
  90. def fetch(self, core=False):
  91. """Build a call to the real bootstrap or core download entry point."""
  92. name = "fn_fetch_file_github" if core else "fn_bootstrap_fetch_file_github"
  93. return f'{name} lgsm/modules sample.sh "$TEST_DIR" nochmodx norun noforcedl nohash\n'
  94. def test_missing_reference_stops_before_bootstrap_download(self):
  95. """Missing reference stops before bootstrap download."""
  96. result, calls, log, exists = self.run_bash(self.fetch(), HTTP_CODE="404")
  97. self.assertEqual(result.returncode, 1, result.stderr)
  98. self.assertIn(
  99. 'Branch "feature/example" not found on GameServerManagers/LinuxGSM',
  100. result.stdout,
  101. )
  102. self.assertIn('Branch "feature/example"', log)
  103. self.assertEqual(len(calls), 1)
  104. self.assertFalse(exists)
  105. def test_missing_reference_with_loaded_core_exits_nonzero(self):
  106. """Missing reference with loaded core exits nonzero."""
  107. code = "core_exit.sh() { echo unexpected-exit-module; exit 0; }\n" + self.fetch(
  108. True
  109. )
  110. result, calls, _, exists = self.run_bash(code, core=True, HTTP_CODE="404")
  111. self.assertEqual(result.returncode, 1, result.stderr)
  112. self.assertEqual(len(calls), 1)
  113. self.assertFalse(exists)
  114. def test_valid_reference_is_checked_once_across_bootstrap_and_update(self):
  115. """Valid reference is checked once across bootstrap and update."""
  116. result, calls, _, exists = self.run_bash(self.fetch() + UPDATE)
  117. self.assertEqual(result.returncode, 0, result.stderr)
  118. probes = [call for call in calls if "api.github.com" in call]
  119. self.assertEqual(len(probes), 1)
  120. self.assertIn("--connect-timeout 3 --max-time 10", probes[0])
  121. self.assertIn("--data-urlencode ref=feature/example", probes[0])
  122. self.assertTrue(exists)
  123. def test_non404_failures_keep_bitbucket_fallback(self):
  124. """Non404 failures keep bitbucket fallback."""
  125. for core in (False, True):
  126. for status, exit_code in (
  127. ("403", "0"),
  128. ("429", "0"),
  129. ("500", "0"),
  130. ("503", "0"),
  131. ("000", "28"),
  132. ("000", "6"),
  133. ):
  134. with self.subTest(core=core, status=status, exit_code=exit_code):
  135. result, calls, _, exists = self.run_bash(
  136. self.fetch(core),
  137. core=core,
  138. HTTP_CODE=status,
  139. PROBE_EXIT=exit_code,
  140. PRIMARY_FAIL="1",
  141. )
  142. self.assertEqual(result.returncode, 0, result.stderr)
  143. self.assertTrue(any("bitbucket.org" in call for call in calls))
  144. self.assertTrue(exists)
  145. self.assertNotIn("not found", result.stdout)
  146. def test_cached_file_needs_no_network(self):
  147. """Cached file needs no network."""
  148. for core in (False, True):
  149. with self.subTest(core=core):
  150. result, calls, _, _ = self.run_bash(
  151. 'touch "$TEST_DIR/sample.sh"\n' + self.fetch(core),
  152. core=core,
  153. HTTP_CODE="404",
  154. )
  155. self.assertEqual(result.returncode, 0, result.stderr)
  156. self.assertEqual(calls, [])
  157. def test_update_missing_branch_stops_before_repo_selection(self):
  158. """Update missing branch stops before repo selection."""
  159. result, calls, _, _ = self.run_bash(UPDATE, HTTP_CODE="404")
  160. self.assertEqual(result.returncode, 1, result.stderr)
  161. self.assertEqual(len(calls), 1)
  162. self.assertIn("api.github.com", calls[0])
  163. def test_update_non404_keeps_repo_fallback(self):
  164. """Update non404 keeps repo fallback."""
  165. for status in ("429", "500", "000"):
  166. with self.subTest(status=status):
  167. result, calls, _, _ = self.run_bash(
  168. UPDATE,
  169. HTTP_CODE=status,
  170. PRIMARY_FAIL="1",
  171. )
  172. self.assertEqual(result.returncode, 0, result.stderr)
  173. self.assertTrue(any("bitbucket.org" in call for call in calls))
  174. def test_upstream_master_still_downloads_version_tag(self):
  175. """Upstream master still downloads version tag."""
  176. result, calls, _, _ = self.run_bash(self.fetch(), TEST_BRANCH="master")
  177. self.assertEqual(result.returncode, 0, result.stderr)
  178. self.assertIn("/v26.2.0/lgsm/modules/sample.sh", calls[-1])
  179. def test_fork_master_is_not_version_pinned(self):
  180. """Fork master is not version pinned."""
  181. result, calls, _, _ = self.run_bash(
  182. 'githubuser="Example"\n' + self.fetch(), TEST_BRANCH="master"
  183. )
  184. self.assertEqual(result.returncode, 0, result.stderr)
  185. self.assertIn("/Example/LinuxGSM/master/lgsm/modules/sample.sh", calls[-1])
  186. def test_ref_change_gets_new_validation(self):
  187. """Ref change gets new validation."""
  188. result, calls, _, _ = self.run_bash(
  189. "fn_check_github_branch\ngithubbranch=other\nfn_check_github_branch"
  190. )
  191. self.assertEqual(result.returncode, 0, result.stderr)
  192. self.assertEqual(len(calls), 2)
  193. def test_tags_and_commit_refs_are_passed_unchanged(self):
  194. """Tags and commit refs are passed unchanged."""
  195. for ref in ("v26.2.0", "2a494011fb636642b9a5d1332a79e359586c54f8"):
  196. with self.subTest(ref=ref):
  197. result, calls, _, _ = self.run_bash(self.fetch(), TEST_BRANCH=ref)
  198. self.assertEqual(result.returncode, 0, result.stderr)
  199. self.assertIn("ref=" + ref, calls[0])
  200. self.assertIn("/" + ref + "/", calls[-1])
  201. def test_new_modules_support_older_launchers(self):
  202. """New modules support older launchers."""
  203. for code in (self.fetch(True), UPDATE):
  204. with self.subTest(code=code[:30]):
  205. result, calls, _, _ = self.run_bash(code, core=True, legacy=True)
  206. self.assertEqual(result.returncode, 0, result.stderr)
  207. self.assertFalse(any("api.github.com" in call for call in calls))
  208. def test_forced_cached_download_validates_ref(self):
  209. """A forced download must validate even when the destination exists."""
  210. for core in (False, True):
  211. with self.subTest(core=core):
  212. code = 'touch "$TEST_DIR/sample.sh"\n' + self.fetch(core).replace(
  213. "noforcedl", "forcedl"
  214. )
  215. result, calls, _, _ = self.run_bash(code, core=core, HTTP_CODE="404")
  216. self.assertEqual(result.returncode, 1, result.stderr)
  217. self.assertEqual(len(calls), 1)
  218. def test_unrelated_download_does_not_validate_linuxgsm_ref(self):
  219. """Other GitHub repositories should not depend on LinuxGSM validation."""
  220. code = (
  221. "fn_fetch_file https://raw.githubusercontent.com/Other/Repo/main/file "
  222. '"" GitHub "" "$TEST_DIR" sample.sh nochmodx norun noforce nohash'
  223. )
  224. result, calls, _, exists = self.run_bash(code, core=True, HTTP_CODE="404")
  225. self.assertEqual(result.returncode, 0, result.stderr)
  226. self.assertTrue(exists)
  227. self.assertEqual(len(calls), 1)
  228. self.assertNotIn("api.github.com", calls[0])
  229. if __name__ == "__main__":
  230. unittest.main(verbosity=2)