mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
`tarball_name()` picked the archive from `platform.machine()` alone, so an end-of-life Node.js on Intel macOS would have unpacked a Linux build into /usr/local, Homebrew's prefix there. The upstream install now supports Linux only and stops with a message to install a supported Node.js through the package manager everywhere else. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
164 lines
5.8 KiB
Python
164 lines
5.8 KiB
Python
"""Unit tests for :mod:`utils.install.node`."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
import hashlib
|
|
import tempfile
|
|
import unittest
|
|
import unittest.mock as mock
|
|
from pathlib import Path
|
|
|
|
from utils.install import node as node_mod
|
|
|
|
SCHEDULE = {
|
|
"v20": {"end": "2026-04-30"},
|
|
"v22": {"end": "2027-04-30"},
|
|
"v24": {"end": "2028-04-30"},
|
|
}
|
|
TODAY = dt.date(2026, 10, 21)
|
|
|
|
|
|
class TestLineIsSupported(unittest.TestCase):
|
|
def test_line_before_its_end_is_supported(self) -> None:
|
|
self.assertTrue(node_mod.line_is_supported("v22", SCHEDULE, TODAY))
|
|
|
|
def test_line_on_its_end_date_is_supported(self) -> None:
|
|
self.assertTrue(
|
|
node_mod.line_is_supported("v20", SCHEDULE, dt.date(2026, 4, 30))
|
|
)
|
|
|
|
def test_line_past_its_end_is_not_supported(self) -> None:
|
|
self.assertFalse(node_mod.line_is_supported("v20", SCHEDULE, TODAY))
|
|
|
|
def test_unknown_line_is_not_supported(self) -> None:
|
|
self.assertFalse(node_mod.line_is_supported("v18", SCHEDULE, TODAY))
|
|
|
|
|
|
class TestNewestLts(unittest.TestCase):
|
|
def test_skips_current_releases(self) -> None:
|
|
index = [
|
|
{"version": "v26.1.0", "lts": False},
|
|
{"version": "v24.22.1", "lts": "Krypton"},
|
|
{"version": "v22.30.0", "lts": "Jod"},
|
|
]
|
|
self.assertEqual(node_mod.newest_lts(index), "v24.22.1")
|
|
|
|
def test_raises_without_lts(self) -> None:
|
|
with self.assertRaises(RuntimeError):
|
|
node_mod.newest_lts([{"version": "v26.1.0", "lts": False}])
|
|
|
|
|
|
class TestTarballName(unittest.TestCase):
|
|
def test_maps_x86_64(self) -> None:
|
|
self.assertEqual(
|
|
node_mod.tarball_name("v24.22.1", "Linux", "x86_64"),
|
|
"node-v24.22.1-linux-x64.tar.gz",
|
|
)
|
|
|
|
def test_maps_aarch64(self) -> None:
|
|
self.assertEqual(
|
|
node_mod.tarball_name("v24.22.1", "Linux", "aarch64"),
|
|
"node-v24.22.1-linux-arm64.tar.gz",
|
|
)
|
|
|
|
def test_raises_for_unknown_machine(self) -> None:
|
|
with self.assertRaises(RuntimeError):
|
|
node_mod.tarball_name("v24.22.1", "Linux", "riscv64")
|
|
|
|
def test_refuses_a_host_that_is_not_linux(self) -> None:
|
|
with self.assertRaises(RuntimeError):
|
|
node_mod.tarball_name("v24.22.1", "Darwin", "x86_64")
|
|
|
|
|
|
class TestExpectedSha256(unittest.TestCase):
|
|
def test_finds_the_listed_file(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
shasums = Path(tmp) / "SHASUMS256.txt"
|
|
shasums.write_text(
|
|
"aaa node-v24.22.1-linux-arm64.tar.gz\n"
|
|
"bbb node-v24.22.1-linux-x64.tar.gz\n",
|
|
encoding="utf-8",
|
|
)
|
|
self.assertEqual(
|
|
node_mod.expected_sha256(shasums, "node-v24.22.1-linux-x64.tar.gz"),
|
|
"bbb",
|
|
)
|
|
|
|
def test_raises_for_an_unlisted_file(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
shasums = Path(tmp) / "SHASUMS256.txt"
|
|
shasums.write_text("aaa other.tar.gz\n", encoding="utf-8")
|
|
with self.assertRaises(RuntimeError):
|
|
node_mod.expected_sha256(shasums, "node-v24.22.1-linux-x64.tar.gz")
|
|
|
|
|
|
class TestInstallUpstreamLts(unittest.TestCase):
|
|
def test_refuses_a_tarball_whose_checksum_differs(self) -> None:
|
|
name = "node-v24.22.1-linux-x64.tar.gz"
|
|
|
|
def fake_download(url: str, output: str, **_kwargs: object) -> None:
|
|
path = Path(output)
|
|
if url.endswith("index.json"):
|
|
path.write_text(
|
|
'[{"version": "v24.22.1", "lts": "Krypton"}]', encoding="utf-8"
|
|
)
|
|
elif url.endswith("SHASUMS256.txt"):
|
|
listed = hashlib.sha256(b"published").hexdigest()
|
|
path.write_text(f"{listed} {name}\n", encoding="utf-8")
|
|
else:
|
|
path.write_bytes(b"tampered")
|
|
|
|
with (
|
|
mock.patch.object(node_mod, "download_file", side_effect=fake_download),
|
|
mock.patch.object(node_mod, "_verify_signature"),
|
|
mock.patch.object(node_mod.platform, "system", return_value="Linux"),
|
|
mock.patch.object(node_mod.platform, "machine", return_value="x86_64"),
|
|
mock.patch.object(node_mod, "run_privileged") as extract,
|
|
self.assertRaises(RuntimeError),
|
|
):
|
|
node_mod.install_upstream_lts()
|
|
extract.assert_not_called()
|
|
|
|
|
|
class TestEnsureNodePresent(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
node_mod.ensure_node_present.cache_clear()
|
|
self.addCleanup(node_mod.ensure_node_present.cache_clear)
|
|
|
|
def _run(self, line: str) -> mock.MagicMock:
|
|
def fake_download(_url: str, output: str, **_kwargs: object) -> None:
|
|
Path(output).write_text(
|
|
'{"v20": {"end": "2026-04-30"}, "v22": {"end": "2099-04-30"}}',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with (
|
|
mock.patch.object(node_mod, "ensure_command_present") as ensure,
|
|
mock.patch.object(node_mod, "installed_line", return_value=line),
|
|
mock.patch.object(node_mod, "download_file", side_effect=fake_download),
|
|
mock.patch.object(node_mod, "install_upstream_lts") as upstream,
|
|
):
|
|
node_mod.ensure_node_present()
|
|
ensure.assert_called_once_with("node")
|
|
return upstream
|
|
|
|
def test_keeps_a_supported_distro_node(self) -> None:
|
|
self._run("v22").assert_not_called()
|
|
|
|
def test_replaces_an_end_of_life_distro_node(self) -> None:
|
|
self._run("v20").assert_called_once_with()
|
|
|
|
def test_propagates_the_hard_failure(self) -> None:
|
|
with (
|
|
mock.patch.object(
|
|
node_mod, "ensure_command_present", side_effect=RuntimeError("boom")
|
|
),
|
|
self.assertRaises(RuntimeError),
|
|
):
|
|
node_mod.ensure_node_present()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|