test_generate_destinations.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. """Tests for generation destination resolution."""
  2. from __future__ import annotations
  3. from pathlib import Path
  4. from typing import Any
  5. import pytest
  6. from cli.core.input import InputManager
  7. from cli.core.module.generation_destination import (
  8. build_remote_shell_path,
  9. build_scp_remote_target,
  10. prompt_generation_destination,
  11. resolve_cli_destination,
  12. resolve_remote_upload_target,
  13. )
  14. def test_resolve_cli_destination_uses_remote_defaults() -> None:
  15. """Remote generation should default the remote path to the template slug."""
  16. destination = resolve_cli_destination(
  17. output=None,
  18. remote="deploy",
  19. remote_path=None,
  20. slug="whoami",
  21. )
  22. assert destination is not None
  23. assert destination.is_remote is True
  24. assert destination.remote_host == "deploy"
  25. assert destination.remote_path == "~/whoami"
  26. def test_resolve_cli_destination_rejects_mixed_local_and_remote_flags() -> None:
  27. """Local and remote destination flags are mutually exclusive."""
  28. with pytest.raises(ValueError, match="either --output"):
  29. resolve_cli_destination(
  30. output="./out",
  31. remote="deploy",
  32. remote_path=None,
  33. slug="whoami",
  34. )
  35. def test_build_remote_shell_path_quotes_home_paths_with_spaces() -> None:
  36. """SSH mkdir paths should stay safe when the target contains spaces."""
  37. assert build_remote_shell_path("~/My Dir/app", trailing_slash=True) == "\"$HOME\"/'My Dir/app/'"
  38. def test_build_scp_remote_target_preserves_home_expansion() -> None:
  39. """SCP targets should quote already-resolved absolute paths safely."""
  40. assert (
  41. build_scp_remote_target("deploy", "/home/test/My Dir/app", trailing_slash=True)
  42. == "deploy:'/home/test/My Dir/app/'"
  43. )
  44. def test_resolve_remote_upload_target_expands_home_via_ssh(monkeypatch: pytest.MonkeyPatch) -> None:
  45. """SCP uploads should resolve ~ to the remote home directory before copying."""
  46. class _Result:
  47. def __init__(self, returncode: int, stdout: str = "", stderr: str = "") -> None:
  48. self.returncode = returncode
  49. self.stdout = stdout
  50. self.stderr = stderr
  51. def fake_run(args: list[str], check: bool, capture_output: bool, text: bool) -> _Result:
  52. del check, capture_output, text
  53. assert args == ["ssh", "deploy", "printf '%s' \"$HOME\""]
  54. return _Result(returncode=0, stdout="/home/deploy")
  55. monkeypatch.setattr("cli.core.module.generation_destination.subprocess.run", fake_run)
  56. assert resolve_remote_upload_target("deploy", "~/dockhand", trailing_slash=True) == "deploy:/home/deploy/dockhand/"
  57. def test_numbered_choice_accepts_numeric_selection(monkeypatch: pytest.MonkeyPatch) -> None:
  58. """Numbered choices should accept the displayed index."""
  59. responses = iter(["2"])
  60. def fake_ask(*args: Any, **kwargs: Any) -> str:
  61. del args, kwargs
  62. return next(responses)
  63. monkeypatch.setattr("cli.core.input.input_manager.Prompt.ask", fake_ask)
  64. input_mgr = InputManager()
  65. assert input_mgr.numbered_choice("Store generated template in", ["local", "remote"], default="local") == "remote"
  66. def test_numbered_choice_uses_numeric_default(monkeypatch: pytest.MonkeyPatch) -> None:
  67. """Numbered choices should display a clean numeric default."""
  68. captured: dict[str, Any] = {}
  69. def fake_ask(prompt: str, default: str = "", show_default: bool = False, **kwargs: Any) -> str:
  70. del prompt, kwargs
  71. captured["default"] = default
  72. captured["show_default"] = show_default
  73. return ""
  74. monkeypatch.setattr("cli.core.input.input_manager.Prompt.ask", fake_ask)
  75. input_mgr = InputManager()
  76. assert input_mgr.numbered_choice("Store generated template in", ["local", "remote"], default="local") == "local"
  77. assert captured == {"default": "1", "show_default": True}
  78. def test_prompt_generation_destination_uses_numbered_choice_for_local(
  79. monkeypatch: pytest.MonkeyPatch,
  80. tmp_path: Path,
  81. ) -> None:
  82. """Interactive destination selection should resolve local output via numbered choices."""
  83. calls: list[tuple[str, tuple[Any, ...], dict[str, Any]]] = []
  84. def fake_numbered_choice(self, prompt: str, choices: list[str], default: str | None = None) -> str:
  85. calls.append(("numbered_choice", (prompt, tuple(choices)), {"default": default}))
  86. del self
  87. return "local"
  88. def fake_text(self, prompt: str, default: str | None = None, **kwargs: Any) -> str:
  89. calls.append(("text", (prompt,), {"default": default, **kwargs}))
  90. del self
  91. return str(tmp_path / "whoami")
  92. monkeypatch.setattr(InputManager, "numbered_choice", fake_numbered_choice)
  93. monkeypatch.setattr(InputManager, "text", fake_text)
  94. destination = prompt_generation_destination("whoami")
  95. assert destination.mode == "local"
  96. assert destination.local_output_dir == tmp_path / "whoami"
  97. assert calls[0] == (
  98. "numbered_choice",
  99. ("Store generated template in", ("local", "remote")),
  100. {"default": "local"},
  101. )
  102. assert calls[1] == (
  103. "text",
  104. ("Local output directory",),
  105. {"default": str(Path.cwd() / "whoami")},
  106. )
  107. def test_prompt_generation_destination_asks_for_remote_host_and_path(monkeypatch: pytest.MonkeyPatch) -> None:
  108. """Interactive remote destination should use direct text prompts for host and path."""
  109. calls: list[tuple[str, tuple[Any, ...], dict[str, Any]]] = []
  110. responses = iter(["srv-test-1.home.clcreative.de", "~/dockhand"])
  111. def fake_numbered_choice(self, prompt: str, choices: list[str], default: str | None = None) -> str:
  112. calls.append(("numbered_choice", (prompt, tuple(choices)), {"default": default}))
  113. del self
  114. return "remote"
  115. def fake_text(self, prompt: str, default: str | None = None, **kwargs: Any) -> str:
  116. calls.append(("text", (prompt,), {"default": default, **kwargs}))
  117. del self
  118. return next(responses)
  119. monkeypatch.setattr(InputManager, "numbered_choice", fake_numbered_choice)
  120. monkeypatch.setattr(InputManager, "text", fake_text)
  121. destination = prompt_generation_destination("dockhand")
  122. assert destination.mode == "remote"
  123. assert destination.remote_host == "srv-test-1.home.clcreative.de"
  124. assert destination.remote_path == "~/dockhand"
  125. assert calls == [
  126. ("numbered_choice", ("Store generated template in", ("local", "remote")), {"default": "local"}),
  127. ("text", ("Remote server host or IP address",), {"default": None}),
  128. ("text", ("Remote target directory",), {"default": "~/dockhand"}),
  129. ]