feat: initial sunbeam CLI package
stdlib-only Python CLI replacing infrastructure/scripts/sunbeam.py. Verbs: up, down, status, apply, seed, verify, logs, restart, get, build, mirror, bootstrap. Service scoping via ns/name target syntax. Auto-bundled kubectl/kustomize/helm (SHA256-verified, cached in ~/.local/share/sunbeam/bin). 63 unittest tests, all passing.
This commit is contained in:
128
sunbeam/tests/test_services.py
Normal file
128
sunbeam/tests/test_services.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Tests for services.py — status scoping, log command construction, restart."""
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch, call
|
||||
|
||||
|
||||
class TestCmdStatus(unittest.TestCase):
|
||||
def test_all_namespaces_when_no_target(self):
|
||||
fake_output = (
|
||||
"ory hydra-abc 1/1 Running 0 1d\n"
|
||||
"data valkey-xyz 1/1 Running 0 1d\n"
|
||||
)
|
||||
with patch("sunbeam.services._capture_out", return_value=fake_output):
|
||||
from sunbeam import services
|
||||
services.cmd_status(None)
|
||||
|
||||
def test_namespace_scoped(self):
|
||||
fake_output = "ory kratos-abc 1/1 Running 0 1d\n"
|
||||
with patch("sunbeam.services._capture_out", return_value=fake_output) as mock_co:
|
||||
from sunbeam import services
|
||||
services.cmd_status("ory")
|
||||
# Should have called _capture_out with -n ory
|
||||
calls_str = str(mock_co.call_args_list)
|
||||
self.assertIn("ory", calls_str)
|
||||
|
||||
def test_pod_scoped(self):
|
||||
fake_output = "kratos-abc 1/1 Running 0 1d\n"
|
||||
with patch("sunbeam.services._capture_out", return_value=fake_output) as mock_co:
|
||||
from sunbeam import services
|
||||
services.cmd_status("ory/kratos")
|
||||
calls_str = str(mock_co.call_args_list)
|
||||
self.assertIn("ory", calls_str)
|
||||
self.assertIn("kratos", calls_str)
|
||||
|
||||
|
||||
class TestCmdLogs(unittest.TestCase):
|
||||
def test_logs_no_follow(self):
|
||||
with patch("subprocess.Popen") as mock_popen:
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.wait.return_value = 0
|
||||
mock_popen.return_value = mock_proc
|
||||
with patch("sunbeam.tools.ensure_tool", return_value="/fake/kubectl"):
|
||||
from sunbeam import services
|
||||
services.cmd_logs("ory/kratos", follow=False)
|
||||
args = mock_popen.call_args[0][0]
|
||||
self.assertIn("-n", args)
|
||||
self.assertIn("ory", args)
|
||||
self.assertNotIn("--follow", args)
|
||||
|
||||
def test_logs_follow(self):
|
||||
with patch("subprocess.Popen") as mock_popen:
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.wait.return_value = 0
|
||||
mock_popen.return_value = mock_proc
|
||||
with patch("sunbeam.tools.ensure_tool", return_value="/fake/kubectl"):
|
||||
from sunbeam import services
|
||||
services.cmd_logs("ory/kratos", follow=True)
|
||||
args = mock_popen.call_args[0][0]
|
||||
self.assertIn("--follow", args)
|
||||
|
||||
def test_logs_requires_service_name(self):
|
||||
"""Passing just a namespace (no service) should die()."""
|
||||
with self.assertRaises(SystemExit):
|
||||
from sunbeam import services
|
||||
services.cmd_logs("ory", follow=False)
|
||||
|
||||
|
||||
class TestCmdGet(unittest.TestCase):
|
||||
def test_prints_yaml_for_pod(self):
|
||||
with patch("sunbeam.services.kube_out", return_value="apiVersion: v1\nkind: Pod") as mock_ko:
|
||||
from sunbeam import services
|
||||
services.cmd_get("ory/kratos-abc")
|
||||
mock_ko.assert_called_once_with("get", "pod", "kratos-abc", "-n", "ory", "-o=yaml")
|
||||
|
||||
def test_default_output_is_yaml(self):
|
||||
with patch("sunbeam.services.kube_out", return_value="kind: Pod"):
|
||||
from sunbeam import services
|
||||
# no output kwarg → defaults to yaml
|
||||
services.cmd_get("ory/kratos-abc")
|
||||
|
||||
def test_json_output_format(self):
|
||||
with patch("sunbeam.services.kube_out", return_value='{"kind":"Pod"}') as mock_ko:
|
||||
from sunbeam import services
|
||||
services.cmd_get("ory/kratos-abc", output="json")
|
||||
mock_ko.assert_called_once_with("get", "pod", "kratos-abc", "-n", "ory", "-o=json")
|
||||
|
||||
def test_missing_name_exits(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
from sunbeam import services
|
||||
services.cmd_get("ory") # namespace-only, no pod name
|
||||
|
||||
def test_not_found_exits(self):
|
||||
with patch("sunbeam.services.kube_out", return_value=""):
|
||||
with self.assertRaises(SystemExit):
|
||||
from sunbeam import services
|
||||
services.cmd_get("ory/nonexistent")
|
||||
|
||||
|
||||
class TestCmdRestart(unittest.TestCase):
|
||||
def test_restart_all(self):
|
||||
with patch("sunbeam.services.kube") as mock_kube:
|
||||
from sunbeam import services
|
||||
services.cmd_restart(None)
|
||||
# Should restart all SERVICES_TO_RESTART
|
||||
self.assertGreater(mock_kube.call_count, 0)
|
||||
|
||||
def test_restart_namespace_scoped(self):
|
||||
with patch("sunbeam.services.kube") as mock_kube:
|
||||
from sunbeam import services
|
||||
services.cmd_restart("ory")
|
||||
calls_str = str(mock_kube.call_args_list)
|
||||
# Should only restart ory/* services
|
||||
self.assertIn("ory", calls_str)
|
||||
self.assertNotIn("devtools", calls_str)
|
||||
|
||||
def test_restart_specific_service(self):
|
||||
with patch("sunbeam.services.kube") as mock_kube:
|
||||
from sunbeam import services
|
||||
services.cmd_restart("ory/kratos")
|
||||
# Should restart exactly deployment/kratos in ory
|
||||
calls_str = str(mock_kube.call_args_list)
|
||||
self.assertIn("kratos", calls_str)
|
||||
|
||||
def test_restart_unknown_service_warns(self):
|
||||
with patch("sunbeam.services.kube") as mock_kube:
|
||||
from sunbeam import services
|
||||
services.cmd_restart("nonexistent/nosuch")
|
||||
# kube should not be called since no match
|
||||
mock_kube.assert_not_called()
|
||||
Reference in New Issue
Block a user