Files
cli/sunbeam/tests/test_tools.py
Sienna Meridian Satterwhite cdc109d728 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.
2026-03-02 20:59:57 +00:00

163 lines
6.9 KiB
Python

"""Tests for tools.py binary bundler."""
import hashlib
import stat
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
import tempfile
import shutil
class TestSha256(unittest.TestCase):
def test_computes_correct_hash(self):
from sunbeam.tools import _sha256
with tempfile.NamedTemporaryFile(delete=False) as f:
f.write(b"hello world")
f.flush()
path = Path(f.name)
try:
expected = hashlib.sha256(b"hello world").hexdigest()
self.assertEqual(_sha256(path), expected)
finally:
path.unlink()
class TestEnsureTool(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.mkdtemp()
self.cache_patcher = patch("sunbeam.tools.CACHE_DIR", Path(self.tmpdir))
self.cache_patcher.start()
def tearDown(self):
self.cache_patcher.stop()
shutil.rmtree(self.tmpdir, ignore_errors=True)
def test_returns_cached_if_sha_matches(self):
binary_data = b"#!/bin/sh\necho kubectl"
dest = Path(self.tmpdir) / "kubectl"
dest.write_bytes(binary_data)
dest.chmod(dest.stat().st_mode | stat.S_IXUSR)
expected_sha = hashlib.sha256(binary_data).hexdigest()
tools_spec = {"kubectl": {"url": "http://x", "sha256": expected_sha}}
with patch("sunbeam.tools.TOOLS", tools_spec):
from sunbeam import tools
result = tools.ensure_tool("kubectl")
self.assertEqual(result, dest)
def test_returns_cached_if_sha_empty(self):
binary_data = b"#!/bin/sh\necho kubectl"
dest = Path(self.tmpdir) / "kubectl"
dest.write_bytes(binary_data)
dest.chmod(dest.stat().st_mode | stat.S_IXUSR)
tools_spec = {"kubectl": {"url": "http://x", "sha256": ""}}
with patch("sunbeam.tools.TOOLS", tools_spec):
from sunbeam import tools
result = tools.ensure_tool("kubectl")
self.assertEqual(result, dest)
def test_downloads_on_cache_miss(self):
binary_data = b"#!/bin/sh\necho kubectl"
tools_spec = {"kubectl": {"url": "http://example.com/kubectl", "sha256": ""}}
with patch("sunbeam.tools.TOOLS", tools_spec):
with patch("urllib.request.urlopen") as mock_url:
mock_resp = MagicMock()
mock_resp.read.return_value = binary_data
mock_resp.__enter__ = lambda s: s
mock_resp.__exit__ = MagicMock(return_value=False)
mock_url.return_value = mock_resp
from sunbeam import tools
result = tools.ensure_tool("kubectl")
dest = Path(self.tmpdir) / "kubectl"
self.assertTrue(dest.exists())
self.assertEqual(dest.read_bytes(), binary_data)
# Should be executable
self.assertTrue(dest.stat().st_mode & stat.S_IXUSR)
def test_raises_on_sha256_mismatch(self):
binary_data = b"#!/bin/sh\necho fake"
tools_spec = {"kubectl": {
"url": "http://example.com/kubectl",
"sha256": "a" * 64, # wrong hash
}}
with patch("sunbeam.tools.TOOLS", tools_spec):
with patch("urllib.request.urlopen") as mock_url:
mock_resp = MagicMock()
mock_resp.read.return_value = binary_data
mock_resp.__enter__ = lambda s: s
mock_resp.__exit__ = MagicMock(return_value=False)
mock_url.return_value = mock_resp
from sunbeam import tools
with self.assertRaises(RuntimeError) as ctx:
tools.ensure_tool("kubectl")
self.assertIn("SHA256 mismatch", str(ctx.exception))
# Binary should be cleaned up
self.assertFalse((Path(self.tmpdir) / "kubectl").exists())
def test_redownloads_on_sha_mismatch_cached(self):
"""If cached binary has wrong hash, it's deleted and re-downloaded."""
old_data = b"old binary"
new_data = b"new binary"
dest = Path(self.tmpdir) / "kubectl"
dest.write_bytes(old_data)
new_sha = hashlib.sha256(new_data).hexdigest()
tools_spec = {"kubectl": {"url": "http://x/kubectl", "sha256": new_sha}}
with patch("sunbeam.tools.TOOLS", tools_spec):
with patch("urllib.request.urlopen") as mock_url:
mock_resp = MagicMock()
mock_resp.read.return_value = new_data
mock_resp.__enter__ = lambda s: s
mock_resp.__exit__ = MagicMock(return_value=False)
mock_url.return_value = mock_resp
from sunbeam import tools
result = tools.ensure_tool("kubectl")
self.assertEqual(dest.read_bytes(), new_data)
def test_unknown_tool_raises_value_error(self):
from sunbeam import tools
with self.assertRaises(ValueError):
tools.ensure_tool("notarealtool")
class TestRunTool(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.mkdtemp()
self.cache_patcher = patch("sunbeam.tools.CACHE_DIR", Path(self.tmpdir))
self.cache_patcher.start()
def tearDown(self):
self.cache_patcher.stop()
shutil.rmtree(self.tmpdir, ignore_errors=True)
def test_kustomize_prepends_cache_dir_to_path(self):
binary_data = b"#!/bin/sh"
dest = Path(self.tmpdir) / "kustomize"
dest.write_bytes(binary_data)
dest.chmod(dest.stat().st_mode | stat.S_IXUSR)
tools_spec = {"kustomize": {"url": "http://x", "sha256": ""}}
with patch("sunbeam.tools.TOOLS", tools_spec):
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
from sunbeam import tools
tools.run_tool("kustomize", "build", ".")
call_kwargs = mock_run.call_args[1]
env = call_kwargs.get("env", {})
self.assertTrue(env.get("PATH", "").startswith(str(self.tmpdir)))
def test_non_kustomize_does_not_modify_path(self):
binary_data = b"#!/bin/sh"
dest = Path(self.tmpdir) / "kubectl"
dest.write_bytes(binary_data)
dest.chmod(dest.stat().st_mode | stat.S_IXUSR)
tools_spec = {"kubectl": {"url": "http://x", "sha256": ""}}
with patch("sunbeam.tools.TOOLS", tools_spec):
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
from sunbeam import tools
import os
original_path = os.environ.get("PATH", "")
tools.run_tool("kubectl", "get", "pods")
call_kwargs = mock_run.call_args[1]
env = call_kwargs.get("env", {})
# PATH should not be modified (starts same as original)
self.assertFalse(env.get("PATH", "").startswith(str(self.tmpdir)))