216 lines
6.9 KiB
Python
216 lines
6.9 KiB
Python
"""Tests for flow.commands.bootstrap helpers and schema behavior."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from flow.commands.bootstrap import (
|
|
_ensure_required_variables,
|
|
_get_profiles,
|
|
_install_binary_package,
|
|
_normalize_profile_package_entry,
|
|
_resolve_package_manager,
|
|
_resolve_package_spec,
|
|
_resolve_pkg_source_name,
|
|
)
|
|
from flow.core.config import AppConfig, FlowContext
|
|
from flow.core.console import ConsoleLogger
|
|
from flow.core.platform import PlatformInfo
|
|
|
|
|
|
@pytest.fixture
|
|
def ctx():
|
|
return FlowContext(
|
|
config=AppConfig(),
|
|
manifest={
|
|
"packages": [
|
|
{
|
|
"name": "fd",
|
|
"type": "pkg",
|
|
"sources": {"apt": "fd-find", "dnf": "fd-find", "brew": "fd"},
|
|
},
|
|
{
|
|
"name": "neovim",
|
|
"type": "binary",
|
|
"source": "github:neovim/neovim",
|
|
"version": "0.10.4",
|
|
"asset-pattern": "nvim-{{os}}-{{arch}}.tar.gz",
|
|
"platform-map": {"linux-x64": {"os": "linux", "arch": "x64"}},
|
|
"install": {"bin": ["bin/nvim"]},
|
|
},
|
|
]
|
|
},
|
|
platform=PlatformInfo(os="linux", arch="x64", platform="linux-x64"),
|
|
console=ConsoleLogger(),
|
|
)
|
|
|
|
|
|
def test_get_profiles_from_manifest(ctx):
|
|
ctx.manifest = {"profiles": {"linux": {"os": "linux"}}}
|
|
assert "linux" in _get_profiles(ctx)
|
|
|
|
|
|
def test_get_profiles_rejects_environments(ctx):
|
|
ctx.manifest = {"environments": {"legacy": {"os": "linux"}}}
|
|
with pytest.raises(RuntimeError, match="no longer supported"):
|
|
_get_profiles(ctx)
|
|
|
|
|
|
def test_resolve_package_manager_explicit_value(ctx):
|
|
assert _resolve_package_manager(ctx, {"os": "linux", "package-manager": "dnf"}) == "dnf"
|
|
|
|
|
|
def test_resolve_package_manager_linux_auto_apt(monkeypatch, ctx):
|
|
monkeypatch.setattr("flow.commands.bootstrap.shutil.which", lambda name: "/usr/bin/apt" if name == "apt" else None)
|
|
assert _resolve_package_manager(ctx, {"os": "linux"}) == "apt"
|
|
|
|
|
|
def test_resolve_package_manager_linux_auto_dnf(monkeypatch, ctx):
|
|
monkeypatch.setattr("flow.commands.bootstrap.shutil.which", lambda name: "/usr/bin/dnf" if name == "dnf" else None)
|
|
assert _resolve_package_manager(ctx, {"os": "linux"}) == "dnf"
|
|
|
|
|
|
def test_resolve_package_manager_requires_os(ctx):
|
|
with pytest.raises(RuntimeError, match="must be set"):
|
|
_resolve_package_manager(ctx, {})
|
|
|
|
|
|
def test_normalize_package_entry_string():
|
|
assert _normalize_profile_package_entry("git") == {"name": "git"}
|
|
|
|
|
|
def test_normalize_package_entry_type_prefix():
|
|
assert _normalize_profile_package_entry("cask/wezterm") == {"name": "wezterm", "type": "cask"}
|
|
|
|
|
|
def test_normalize_package_entry_object():
|
|
out = _normalize_profile_package_entry({"name": "docker", "allow_sudo": True})
|
|
assert out["name"] == "docker"
|
|
assert out["allow_sudo"] is True
|
|
|
|
|
|
def test_resolve_package_spec_uses_catalog_type(ctx):
|
|
catalog = {
|
|
"fd": {
|
|
"name": "fd",
|
|
"type": "pkg",
|
|
"sources": {"apt": "fd-find"},
|
|
}
|
|
}
|
|
resolved = _resolve_package_spec(catalog, {"name": "fd"})
|
|
assert resolved["type"] == "pkg"
|
|
assert resolved["sources"]["apt"] == "fd-find"
|
|
|
|
|
|
def test_resolve_package_spec_defaults_to_pkg(ctx):
|
|
resolved = _resolve_package_spec({}, {"name": "git"})
|
|
assert resolved["type"] == "pkg"
|
|
|
|
|
|
def test_resolve_package_spec_profile_override(ctx):
|
|
catalog = {
|
|
"neovim": {
|
|
"name": "neovim",
|
|
"type": "binary",
|
|
"version": "0.10.4",
|
|
}
|
|
}
|
|
resolved = _resolve_package_spec(catalog, {"name": "neovim", "post-install": "echo ok"})
|
|
assert resolved["type"] == "binary"
|
|
assert resolved["post-install"] == "echo ok"
|
|
|
|
|
|
def test_resolve_pkg_source_name_with_mapping(ctx):
|
|
spec = {"name": "fd", "sources": {"apt": "fd-find", "dnf": "fd-find", "brew": "fd"}}
|
|
assert _resolve_pkg_source_name(spec, "apt") == "fd-find"
|
|
assert _resolve_pkg_source_name(spec, "dnf") == "fd-find"
|
|
assert _resolve_pkg_source_name(spec, "brew") == "fd"
|
|
|
|
|
|
def test_resolve_pkg_source_name_fallback_to_name(ctx):
|
|
spec = {"name": "ripgrep", "sources": {"apt": "ripgrep"}}
|
|
assert _resolve_pkg_source_name(spec, "dnf") == "ripgrep"
|
|
|
|
|
|
def test_ensure_required_variables_missing_raises():
|
|
with pytest.raises(RuntimeError, match="Missing required environment variables"):
|
|
_ensure_required_variables({"requires": ["USER_EMAIL", "TARGET_HOSTNAME"]}, {"USER_EMAIL": "a@b"})
|
|
|
|
|
|
def test_ensure_required_variables_accepts_vars(monkeypatch):
|
|
env = dict(os.environ)
|
|
env["USER_EMAIL"] = "a@b"
|
|
env["TARGET_HOSTNAME"] = "devbox"
|
|
_ensure_required_variables({"requires": ["USER_EMAIL", "TARGET_HOSTNAME"]}, env)
|
|
|
|
|
|
class _FakeResponse:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
def read(self):
|
|
return b"archive"
|
|
|
|
|
|
def _patch_binary_download(monkeypatch, after_unpack=None):
|
|
monkeypatch.setattr(
|
|
"flow.commands.bootstrap.urllib.request.urlopen",
|
|
lambda *args, **kwargs: _FakeResponse(),
|
|
)
|
|
|
|
def _fake_unpack(_archive, extract_dir):
|
|
extracted = Path(extract_dir)
|
|
extracted.mkdir(parents=True, exist_ok=True)
|
|
if after_unpack:
|
|
after_unpack(extracted)
|
|
|
|
monkeypatch.setattr("flow.commands.bootstrap.shutil.unpack_archive", _fake_unpack)
|
|
|
|
|
|
def test_install_binary_package_rejects_absolute_declared_path(monkeypatch, tmp_path, ctx):
|
|
absolute_item = tmp_path / "outside-bin"
|
|
absolute_item.write_text("binary")
|
|
|
|
_patch_binary_download(monkeypatch)
|
|
monkeypatch.setattr(
|
|
"flow.commands.bootstrap._copy_install_item",
|
|
lambda *args, **kwargs: pytest.fail("_copy_install_item should not be called"),
|
|
)
|
|
|
|
spec = {
|
|
"name": "demo",
|
|
"type": "binary",
|
|
"source": "https://example.invalid/demo",
|
|
"asset-pattern": "demo.tar.gz",
|
|
"install": {"bin": [str(absolute_item)]},
|
|
}
|
|
|
|
with pytest.raises(RuntimeError, match="must be relative"):
|
|
_install_binary_package(ctx, spec, {}, dry_run=False)
|
|
|
|
|
|
def test_install_binary_package_rejects_parent_traversal_declared_path(monkeypatch, ctx):
|
|
def _after_unpack(extracted):
|
|
(extracted.parent / "escape-bin").write_text("binary")
|
|
|
|
_patch_binary_download(monkeypatch, after_unpack=_after_unpack)
|
|
monkeypatch.setattr(
|
|
"flow.commands.bootstrap._copy_install_item",
|
|
lambda *args, **kwargs: pytest.fail("_copy_install_item should not be called"),
|
|
)
|
|
|
|
spec = {
|
|
"name": "demo",
|
|
"type": "binary",
|
|
"source": "https://example.invalid/demo",
|
|
"asset-pattern": "demo.tar.gz",
|
|
"install": {"bin": ["../escape-bin"]},
|
|
}
|
|
|
|
with pytest.raises(RuntimeError, match="parent traversal"):
|
|
_install_binary_package(ctx, spec, {}, dry_run=False)
|