Files
computer-playbook/tests/unit/python/utils/install/test_node.py
kevinveenbirkenbachandClaude Opus 5.5 d4b3609488 fix(install): refuse the upstream Node.js install on non-Linux hosts
`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>
2026-10-21 16:52:56 +02:00

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