test_doctor.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. from __future__ import annotations
  2. from pathlib import Path
  3. from core.doctor import DoctorCheck, apply_doctor_fixes, collect_doctor_report, format_doctor_report
  4. from core.settings import AppSettings
  5. def test_collect_doctor_report_marks_lite_true_and_full_false(monkeypatch, tmp_path):
  6. settings = AppSettings.from_env()
  7. settings = AppSettings(
  8. **{
  9. **settings.__dict__,
  10. "env_file": tmp_path / ".env",
  11. "config_dir": tmp_path / "config",
  12. "state_dir": tmp_path / "state",
  13. "log_dir": tmp_path / "logs",
  14. "pool_dir": tmp_path / "pool",
  15. "backend": "cpa",
  16. }
  17. )
  18. settings.env_file.write_text("ZHUCE6_REGISTER_MAIL_PROVIDER=cfmail\n", encoding="utf-8")
  19. monkeypatch.setattr("core.doctor._check_python_version", lambda _settings: DoctorCheck("python", "ok", "Python 版本满足要求"))
  20. monkeypatch.setattr("core.doctor._check_env_file", lambda _settings: DoctorCheck("env", "ok", ".env 可读取"))
  21. monkeypatch.setattr("core.doctor._check_core_dependencies", lambda _settings: DoctorCheck("deps", "ok", "核心依赖齐全"))
  22. monkeypatch.setattr("core.doctor._check_cfmail", lambda _settings: DoctorCheck("cfmail", "ok", "cfmail 配置正常"))
  23. monkeypatch.setattr("core.doctor._check_proxy", lambda _settings: DoctorCheck("proxy", "ok", "代理可用"))
  24. monkeypatch.setattr("core.doctor._check_directory_writable", lambda _settings: DoctorCheck("dirs", "ok", "目录可写"))
  25. monkeypatch.setattr("core.doctor._check_sslocal", lambda _settings: DoctorCheck("sslocal", "skip", "使用 direct proxy URLs, 不依赖 sslocal", required_for=()))
  26. monkeypatch.setattr("core.doctor._check_cpa_management", lambda _settings: DoctorCheck("cpa", "error", "CPA management 不可达"))
  27. monkeypatch.setattr("core.doctor._check_sub2api", lambda _settings: DoctorCheck("sub2api", "skip", "当前 backend 不是 sub2api", required_for=()))
  28. report = collect_doctor_report(settings)
  29. assert report.lite_available is True
  30. assert report.full_available is False
  31. assert report.full_cpa_available is False
  32. assert report.full_sub2api_available is False
  33. text = format_doctor_report(report)
  34. assert "lite: available" in text
  35. assert "full(cpa): unavailable" in text
  36. assert "docker" not in text
  37. def test_collect_doctor_report_marks_sub2api_available(monkeypatch, tmp_path):
  38. settings = AppSettings(
  39. env_file=tmp_path / ".env",
  40. config_dir=tmp_path / "config",
  41. state_dir=tmp_path / "state",
  42. log_dir=tmp_path / "logs",
  43. pool_dir=tmp_path / "pool",
  44. backend="sub2api",
  45. )
  46. settings.env_file.write_text("ZHUCE6_REGISTER_MAIL_PROVIDER=mailtm\n", encoding="utf-8")
  47. monkeypatch.setattr("core.doctor._check_python_version", lambda _settings: DoctorCheck("python", "ok", "Python 版本满足要求"))
  48. monkeypatch.setattr("core.doctor._check_env_file", lambda _settings: DoctorCheck("env", "ok", ".env 可读取"))
  49. monkeypatch.setattr("core.doctor._check_core_dependencies", lambda _settings: DoctorCheck("deps", "ok", "核心依赖齐全"))
  50. monkeypatch.setattr("core.doctor._check_cfmail", lambda _settings: DoctorCheck("cfmail", "skip", "register 未启用 cfmail", required_for=()))
  51. monkeypatch.setattr("core.doctor._check_proxy", lambda _settings: DoctorCheck("proxy", "ok", "代理可用"))
  52. monkeypatch.setattr("core.doctor._check_directory_writable", lambda _settings: DoctorCheck("dirs", "ok", "目录可写"))
  53. monkeypatch.setattr("core.doctor._check_sslocal", lambda _settings: DoctorCheck("sslocal", "skip", "使用 direct proxy URLs, 不依赖 sslocal", required_for=()))
  54. monkeypatch.setattr("core.doctor._check_cpa_management", lambda _settings: DoctorCheck("cpa", "skip", "当前 backend 不是 cpa", required_for=()))
  55. monkeypatch.setattr("core.doctor._check_sub2api", lambda _settings: DoctorCheck("sub2api", "ok", "sub2api 可达"))
  56. report = collect_doctor_report(settings)
  57. assert report.lite_available is True
  58. assert report.full_available is True
  59. assert report.full_sub2api_available is True
  60. assert report.full_cpa_available is False
  61. def test_check_sslocal_missing_in_config_mode_includes_install_guide(tmp_path):
  62. from core.doctor import _check_sslocal
  63. import core.doctor as doctor
  64. settings = AppSettings(proxy_pool_direct_urls="", proxy_pool_config=tmp_path / "clash.yaml")
  65. original_which = doctor.shutil.which
  66. doctor.shutil.which = lambda _name: None
  67. try:
  68. check = _check_sslocal(settings)
  69. finally:
  70. doctor.shutil.which = original_which
  71. assert check.status == "error"
  72. assert "未安装" in check.summary
  73. assert "shadowsocks-rust" in check.detail
  74. assert "Windows:" in check.detail
  75. def test_format_doctor_report_renders_multiline_detail(tmp_path):
  76. settings = AppSettings(env_file=tmp_path / ".env")
  77. report = type(
  78. "FakeReport",
  79. (),
  80. {
  81. "settings": settings,
  82. "checks": (
  83. DoctorCheck("sslocal", "error", "未安装 sslocal", detail="Line A\nLine B"),
  84. ),
  85. "lite_available": False,
  86. "full_available": False,
  87. "full_cpa_available": False,
  88. "full_sub2api_available": False,
  89. },
  90. )()
  91. text = format_doctor_report(report)
  92. assert "Line A" in text
  93. assert "Line B" in text
  94. assert "full(sub2api): unavailable" in text
  95. def test_check_proxy_requires_socksio_when_socks_proxy_configured(monkeypatch):
  96. from core.doctor import _check_proxy
  97. settings = AppSettings(register_proxy="socks5://127.0.0.1:1080", proxy_pool_direct_urls="", proxy_pool_config=None)
  98. original_import_module = __import__("importlib").import_module
  99. def fake_import_module(name: str, package=None): # type: ignore[no-untyped-def]
  100. if name == "socksio":
  101. raise ModuleNotFoundError("No module named 'socksio'")
  102. return original_import_module(name, package)
  103. monkeypatch.setattr("core.doctor.importlib.import_module", fake_import_module)
  104. check = _check_proxy(settings)
  105. assert check.status == "error"
  106. assert "SOCKS" in check.summary
  107. assert "socksio" in check.detail
  108. assert "uv sync" in check.detail
  109. def test_check_core_dependencies_reports_new_required_modules(monkeypatch):
  110. from core.doctor import _check_core_dependencies
  111. original_import_module = __import__("importlib").import_module
  112. def fake_import_module(name: str, package=None): # type: ignore[no-untyped-def]
  113. if name in {"filelock", "psutil", "socksio"}:
  114. raise ModuleNotFoundError(f"No module named {name}")
  115. return original_import_module(name, package)
  116. monkeypatch.setattr("core.doctor.importlib.import_module", fake_import_module)
  117. check = _check_core_dependencies(AppSettings())
  118. assert check.status == "error"
  119. assert "filelock" in check.summary
  120. assert "psutil" in check.summary
  121. assert "socksio" in check.summary
  122. def test_apply_doctor_fixes_runs_uv_sync_and_optional_npm(monkeypatch, tmp_path: Path):
  123. commands: list[tuple[tuple[str, ...], Path]] = []
  124. repo_root = tmp_path
  125. worker_dir = repo_root / "vendor" / "cfmail-worker" / "worker"
  126. worker_dir.mkdir(parents=True)
  127. (worker_dir / "package.json").write_text("{}", encoding="utf-8")
  128. def fake_run(args, cwd, check): # type: ignore[no-untyped-def]
  129. commands.append((tuple(args), Path(cwd)))
  130. return None
  131. monkeypatch.setattr("core.doctor.subprocess.run", fake_run)
  132. settings = AppSettings(project_root=repo_root)
  133. actions = apply_doctor_fixes(settings)
  134. assert commands == [
  135. (("uv", "sync"), repo_root),
  136. (("npm", "install", "--no-fund", "--no-audit"), worker_dir),
  137. ]
  138. assert actions == [f"uv sync @ {repo_root}", f"npm install @ {worker_dir}"]