Files
cli/sunbeam/tests/test_tools.py

163 lines
6.9 KiB
Python
Raw Normal View History

"""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)))