- build-release.sh: POSIX-only checks (dash runs the release), a release form is exactly X.Y.Z or X.Y.Z-alpha|beta|rcN, and the install smoke refuses a package that has no smoke recipe. - Tests: names and tag gate (exact match, committed template must agree), key refusals before any container starts (fake podman records calls), the render script pins only the build tree, podman wiring (only the sign phase sees the key, and it has no network), the xbps ordering of pre-release spellings, the workflow gates, tmpfs key staging, and the publish set (sdist, packages, signatures, repodata, client key).
560 lines
22 KiB
Python
560 lines
22 KiB
Python
"""Offline guards for the one Void release path (spec #16; tickets #28 and #29).
|
|
|
|
``packaging/void/build-release.sh`` is the release path. The Gitea tag workflow,
|
|
the local dry run and the smoke gate (``packaging/void/smoke.sh``) all call it,
|
|
so a passing dry run exercises the code a tag runs. These tests pin its seams:
|
|
|
|
- the xbps spelling a tag maps to, and the tag gate (exact ``v`` plus
|
|
``__version__``, and the committed template must already name that release);
|
|
- the signing-key checks, which must stop before any container starts (a fake
|
|
``podman`` on PATH records any call and must never be reached);
|
|
- ``render-voculinux.sh``, run against a scratch void-packages tree, which pins
|
|
the build tree and never the repository's template;
|
|
- the podman wiring: which phase may see the key, and which phase has no network;
|
|
- the xbps version ordering the mapping depends on (``xbps-uhelper cmpver``,
|
|
skipped where xbps is not installed).
|
|
|
|
The container runs themselves are the proof; these are the guards around them.
|
|
Expected values come from the documented rules, not from the code under test.
|
|
"""
|
|
|
|
import hashlib
|
|
import os
|
|
import plistlib
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
VOID = REPO_ROOT / "packaging" / "void"
|
|
BUILD_RELEASE = VOID / "build-release.sh"
|
|
CONTAINER_RELEASE = VOID / "container-release.sh"
|
|
RENDER = VOID / "render-voculinux.sh"
|
|
FINGERPRINT_PY = VOID / "xbps-fingerprint.py"
|
|
SMOKE_SH = VOID / "smoke.sh"
|
|
PINS = VOID / "tool_checksums.txt"
|
|
PLIST = VOID / "repository-key.plist"
|
|
TEMPLATE = VOID / "srcpkgs" / "voculinux" / "template"
|
|
VERSION_PY = REPO_ROOT / "src" / "vocalinux" / "version.py"
|
|
|
|
PINNED_FINGERPRINT = "fa:d1:00:32:ce:b1:b9:fd:73:05:05:76:75:54:37:25"
|
|
SDIST = "voculinux-0.19.0beta.tar.gz"
|
|
XBPS_UHELPER = shutil.which("xbps-uhelper")
|
|
OPENSSL = shutil.which("openssl")
|
|
|
|
#: Files the release path reads from its own directory, copied into fixtures.
|
|
FIXTURE_FILES = (BUILD_RELEASE, CONTAINER_RELEASE, RENDER, FINGERPRINT_PY, SMOKE_SH, PINS, PLIST)
|
|
|
|
|
|
def _body(text: str, name: str) -> str:
|
|
"""The body of a top-level shell function, from its opening line to the closing brace."""
|
|
match = re.search(rf"^{name}\(\) \{{\n(.*?)^\}}$", text, re.MULTILINE | re.DOTALL)
|
|
assert match, f"{name}() must be a top-level function"
|
|
return match.group(1)
|
|
|
|
|
|
def _code(path: Path) -> str:
|
|
"""A shell or Python script with comment-only lines removed."""
|
|
return "\n".join(
|
|
line
|
|
for line in path.read_text(encoding="utf-8").splitlines()
|
|
if not line.lstrip().startswith("#")
|
|
)
|
|
|
|
|
|
def make_tree(
|
|
root: Path,
|
|
version: str,
|
|
template_version: str,
|
|
template_tag: str,
|
|
version_py: Optional[str] = None,
|
|
) -> Path:
|
|
"""A scratch copy of the release tree, with the given version and committed template."""
|
|
pkg = root / "packaging" / "void"
|
|
pkg.mkdir(parents=True)
|
|
for source in FIXTURE_FILES:
|
|
shutil.copy2(source, pkg / source.name)
|
|
(pkg / "srcpkgs" / "voculinux").mkdir(parents=True)
|
|
text = TEMPLATE.read_text(encoding="utf-8")
|
|
text = re.sub(r"^version=.*$", f"version={template_version}", text, flags=re.MULTILINE)
|
|
text = re.sub(r"^_tag=.*$", f"_tag={template_tag}", text, flags=re.MULTILINE)
|
|
(pkg / "srcpkgs" / "voculinux" / "template").write_text(text, encoding="utf-8")
|
|
src = root / "src" / "vocalinux"
|
|
src.mkdir(parents=True)
|
|
(src / "version.py").write_text(
|
|
version_py if version_py is not None else f'"""Version."""\n\n__version__ = "{version}"\n',
|
|
encoding="utf-8",
|
|
)
|
|
return root
|
|
|
|
|
|
def run_build(tree: Path, *args: str, path: Optional[str] = None, tmp: Optional[Path] = None):
|
|
"""Run the tree's build-release.sh with a minimal, explicit environment."""
|
|
env = {"PATH": path or os.environ["PATH"], "HOME": os.environ.get("HOME", "/")}
|
|
if tmp is not None:
|
|
env["TMPDIR"] = str(tmp)
|
|
return subprocess.run(
|
|
["sh", str(tree / "packaging" / "void" / "build-release.sh"), *args],
|
|
cwd=tree,
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
check=False,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Names: a tag maps to one xbps spelling, and only release forms map at all
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("version", "xbps"),
|
|
[
|
|
("0.18.5", "0.18.5"),
|
|
("1.0.0", "1.0.0"),
|
|
("0.19.0-alpha", "0.19.0alpha"),
|
|
("0.19.0-beta", "0.19.0beta"),
|
|
("0.19.0-rc1", "0.19.0rc1"),
|
|
("0.19.0-rc12", "0.19.0rc12"),
|
|
],
|
|
)
|
|
def test_a_tag_maps_to_its_xbps_spelling(tmp_path: Path, version: str, xbps: str) -> None:
|
|
"""xbps versions have no '-': the hyphen goes, and the rest is unchanged."""
|
|
tree = make_tree(tmp_path, version, template_version=xbps, template_tag=f"v{version}")
|
|
result = run_build(tree, "names", f"v{version}")
|
|
assert result.returncode == 0, result.stderr
|
|
names = dict(line.split("=", 1) for line in result.stdout.splitlines())
|
|
assert names == {
|
|
"TAG": f"v{version}",
|
|
"VERSION": version,
|
|
"XBPS_VERSION": xbps,
|
|
"SDIST_NAME": f"voculinux-{xbps}.tar.gz",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"version",
|
|
[
|
|
"0.19.0-beta.2",
|
|
"0.19.0+local",
|
|
"0.19.0_beta",
|
|
"0.19",
|
|
"0.19.0-dev",
|
|
"0.19.0-post1",
|
|
"0.19.0-beta-2",
|
|
"v0.19.0",
|
|
],
|
|
)
|
|
def test_a_version_outside_the_release_forms_is_refused(tmp_path: Path, version: str) -> None:
|
|
"""Only X.Y.Z and X.Y.Z-alpha|beta|rcN name a release; anything else stops."""
|
|
tree = make_tree(tmp_path, version, template_version="0.19.0", template_tag=f"v{version}")
|
|
result = run_build(tree, "names", f"v{version}")
|
|
assert result.returncode == 1, result.stdout
|
|
assert "not a release form" in result.stderr
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The tag gate: exact match, and the committed template must already agree
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("tag", "version", "ok"),
|
|
[
|
|
("v0.18.5", "0.18.5", True),
|
|
("v0.19.0-beta", "0.19.0-beta", True),
|
|
("0.18.5", "0.18.5", False), # no v prefix
|
|
("v0.18.50", "0.18.5", False), # the tag contains the version
|
|
("v0.18", "0.18.5", False), # the version contains the tag
|
|
("v0.18.5-beta", "0.18.5", False),
|
|
("v0.18.5", "0.18.5-beta", False),
|
|
("v0.18.4", "0.18.5", False),
|
|
("vv0.18.5", "0.18.5", False),
|
|
],
|
|
)
|
|
def test_tag_gate_is_an_exact_match_with_version_py(
|
|
tmp_path: Path, tag: str, version: str, ok: bool
|
|
) -> None:
|
|
xbps = version.replace("-", "")
|
|
tree = make_tree(tmp_path, version, template_version=xbps, template_tag=f"v{version}")
|
|
result = run_build(tree, "check-tag", tag)
|
|
if ok:
|
|
assert result.returncode == 0, result.stderr
|
|
assert "tag gate passed" in result.stdout
|
|
else:
|
|
assert result.returncode == 1, f"{tag} against {version} must stop: {result.stdout}"
|
|
assert "refusing to publish" in result.stderr
|
|
|
|
|
|
def test_the_committed_template_must_name_this_release(tmp_path: Path) -> None:
|
|
"""A release whose template still names the previous version never publishes."""
|
|
tree = make_tree(tmp_path, "0.19.0-beta", template_version="0.18.5", template_tag="v0.18.5")
|
|
result = run_build(tree, "check-tag", "v0.19.0-beta")
|
|
assert result.returncode == 1
|
|
assert "committed template version is '0.18.5', not '0.19.0beta'" in result.stderr
|
|
|
|
|
|
def test_the_committed_template_tag_must_match_the_tag(tmp_path: Path) -> None:
|
|
tree = make_tree(tmp_path, "0.19.0-beta", template_version="0.19.0beta", template_tag="v0.19.0")
|
|
result = run_build(tree, "check-tag", "v0.19.0-beta")
|
|
assert result.returncode == 1
|
|
assert "_tag is 'v0.19.0'" in result.stderr
|
|
|
|
|
|
def test_version_py_must_define_the_version_exactly_once(tmp_path: Path) -> None:
|
|
body = '__version__ = "0.18.5"\n__version__ = "0.18.6"\n'
|
|
tree = make_tree(tmp_path, "0.18.5", "0.18.5", "v0.18.5", version_py=body)
|
|
result = run_build(tree, "check-tag", "v0.18.5")
|
|
assert result.returncode == 1
|
|
assert "exactly once" in result.stderr
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Version ordering: the pre-release spellings sort below the release
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.skipif(XBPS_UHELPER is None, reason="xbps-uhelper is not installed")
|
|
@pytest.mark.parametrize(
|
|
("lower", "higher"),
|
|
[
|
|
("0.18.5beta", "0.18.5"),
|
|
("0.18.5rc1", "0.18.5"),
|
|
("0.18.5alpha", "0.18.5beta"),
|
|
("0.18.5beta", "0.18.5rc1"),
|
|
("0.18.5rc1", "0.18.5rc2"),
|
|
("0.18.5rc2", "0.18.5rc10"),
|
|
("0.18.5", "0.19.0beta"),
|
|
("0.19.0beta", "0.19.0"),
|
|
],
|
|
)
|
|
def test_xbps_sorts_pre_release_spellings_below_the_release(lower: str, higher: str) -> None:
|
|
"""A stable user is never offered a pre-release of the same version as newer."""
|
|
result = subprocess.run(
|
|
[XBPS_UHELPER, "cmpver", f"voculinux-{lower}_1", f"voculinux-{higher}_1"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
# xbps-uhelper exits -1 (255) when the first package is older than the second.
|
|
assert result.returncode == 255, f"{lower} must sort below {higher}: rc={result.returncode}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Signing key: every refusal happens before any container starts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture()
|
|
def fake_podman(tmp_path: Path) -> tuple:
|
|
"""A podman that records its calls and fails. Any call means a container would start."""
|
|
bindir = tmp_path / "fakebin"
|
|
bindir.mkdir()
|
|
log = tmp_path / "podman-calls.log"
|
|
script = bindir / "podman"
|
|
script.write_text(f'#!/bin/sh\necho "$@" >> "{log}"\nexit 97\n', encoding="utf-8")
|
|
script.chmod(0o755)
|
|
return f"{bindir}{os.pathsep}{os.environ['PATH']}", log
|
|
|
|
|
|
def _key(path: Path, bits: int) -> Path:
|
|
subprocess.run(
|
|
[OPENSSL, "genrsa", "-out", str(path), str(bits)],
|
|
capture_output=True,
|
|
check=True,
|
|
timeout=60,
|
|
)
|
|
return path
|
|
|
|
|
|
@pytest.mark.skipif(OPENSSL is None, reason="openssl is not installed")
|
|
@pytest.mark.parametrize(
|
|
("case", "message"),
|
|
[
|
|
("missing-arg", "no signing key"),
|
|
("empty", "missing or empty"),
|
|
("garbage", "not a valid RSA private key"),
|
|
("rsa2048", "not RSA 4096"),
|
|
("rsa4096-other", "does not match packaging/void/repository-key.plist"),
|
|
],
|
|
)
|
|
def test_a_bad_key_stops_the_release_before_any_container(
|
|
tmp_path: Path, fake_podman: tuple, case: str, message: str
|
|
) -> None:
|
|
path, log = fake_podman
|
|
tree = make_tree(tmp_path / "tree", "0.18.5", template_version="0.18.5", template_tag="v0.18.5")
|
|
out = tmp_path / "out"
|
|
args = ["release", "v0.18.5", "--out-dir", str(out)]
|
|
if case == "empty":
|
|
key = tmp_path / "empty.pem"
|
|
key.write_bytes(b"")
|
|
elif case == "garbage":
|
|
key = tmp_path / "garbage.pem"
|
|
key.write_text("not a key\n", encoding="utf-8")
|
|
elif case == "rsa2048":
|
|
key = _key(tmp_path / "rsa2048.pem", 2048)
|
|
elif case == "rsa4096-other":
|
|
key = _key(tmp_path / "other.pem", 4096)
|
|
if case != "missing-arg":
|
|
args += ["--key-file", str(key)]
|
|
tmp = tmp_path / "work"
|
|
tmp.mkdir()
|
|
result = run_build(tree, *args, path=path, tmp=tmp)
|
|
assert result.returncode == 1, result.stdout + result.stderr
|
|
assert message in result.stderr
|
|
assert not log.exists(), "a container started with a refused key"
|
|
assert not out.exists(), "a refused release must not create its output directory"
|
|
for work in tmp.glob("voculinux-release.*"):
|
|
assert not (work / "dist").exists(), "the sdist must not be built for a refused key"
|
|
|
|
|
|
@pytest.mark.skipif(OPENSSL is None, reason="openssl is not installed")
|
|
def test_the_key_file_is_never_printed(tmp_path: Path, fake_podman: tuple) -> None:
|
|
"""Refusals name the problem, never the key material."""
|
|
path, _ = fake_podman
|
|
tree = make_tree(tmp_path / "tree", "0.18.5", template_version="0.18.5", template_tag="v0.18.5")
|
|
key = _key(tmp_path / "other.pem", 4096)
|
|
secret_line = key.read_text(encoding="ascii").splitlines()[1]
|
|
tmp = tmp_path / "work"
|
|
tmp.mkdir()
|
|
result = run_build(
|
|
tree,
|
|
"release",
|
|
"v0.18.5",
|
|
"--key-file",
|
|
str(key),
|
|
"--out-dir",
|
|
str(tmp_path / "o"),
|
|
path=path,
|
|
tmp=tmp,
|
|
)
|
|
assert secret_line not in result.stdout + result.stderr
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# render-voculinux.sh: the build tree is pinned, the repository never is
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _scratch_void_packages(tmp_path: Path) -> Path:
|
|
vp = tmp_path / "void-packages"
|
|
(vp / "srcpkgs" / "voculinux").mkdir(parents=True)
|
|
shutil.copy2(TEMPLATE, vp / "srcpkgs" / "voculinux" / "template")
|
|
return vp
|
|
|
|
|
|
def _render(tmp_path: Path, vp: Path, payload: bytes, **overrides: str):
|
|
inputs = tmp_path / "inputs"
|
|
inputs.mkdir(exist_ok=True)
|
|
(inputs / SDIST).write_bytes(payload)
|
|
env = {
|
|
"PATH": os.environ["PATH"],
|
|
"VOID_PACKAGES": str(vp),
|
|
"SDIST_DIR": str(inputs),
|
|
"XBPS_VERSION": "0.19.0beta",
|
|
"TAG": "v0.19.0-beta",
|
|
"SDIST_NAME": SDIST,
|
|
"SDIST_SHA256": hashlib.sha256(payload).hexdigest(),
|
|
}
|
|
env.update(overrides)
|
|
return subprocess.run(
|
|
["sh", str(RENDER)], env=env, capture_output=True, text=True, timeout=30, check=False
|
|
)
|
|
|
|
|
|
def test_render_pins_version_tag_and_checksum_in_the_build_tree(tmp_path: Path) -> None:
|
|
payload = b"canonical sdist bytes for the release"
|
|
vp = _scratch_void_packages(tmp_path)
|
|
repo_before = TEMPLATE.read_bytes()
|
|
result = _render(tmp_path, vp, payload)
|
|
assert result.returncode == 0, result.stderr
|
|
text = (vp / "srcpkgs" / "voculinux" / "template").read_text(encoding="utf-8")
|
|
digest = hashlib.sha256(payload).hexdigest()
|
|
assert re.search(r"^version=0\.19\.0beta$", text, re.MULTILINE)
|
|
assert re.search(r"^_tag=v0\.19\.0-beta$", text, re.MULTILINE)
|
|
assert re.search(rf"^checksum={digest}$", text, re.MULTILINE)
|
|
assert "releases/download/${_tag}/voculinux-${version}.tar.gz" in text, "distfiles unchanged"
|
|
staged = vp / "hostdir" / "sources" / "voculinux-0.19.0beta" / SDIST
|
|
assert staged.read_bytes() == payload, "xbps-src must find the sdist where it looks"
|
|
assert TEMPLATE.read_bytes() == repo_before, "the repository template must never change"
|
|
|
|
|
|
def test_render_refuses_a_payload_that_does_not_match_its_digest(tmp_path: Path) -> None:
|
|
vp = _scratch_void_packages(tmp_path)
|
|
before = (vp / "srcpkgs" / "voculinux" / "template").read_bytes()
|
|
result = _render(tmp_path, vp, b"payload", SDIST_SHA256="0" * 64)
|
|
assert result.returncode == 1
|
|
assert "is not the expected" in result.stderr
|
|
assert (vp / "srcpkgs" / "voculinux" / "template").read_bytes() == before
|
|
|
|
|
|
def test_render_refuses_a_template_without_exactly_one_checksum_line(tmp_path: Path) -> None:
|
|
vp = _scratch_void_packages(tmp_path)
|
|
template = vp / "srcpkgs" / "voculinux" / "template"
|
|
template.write_text(template.read_text(encoding="utf-8") + "checksum=abc\n", encoding="utf-8")
|
|
result = _render(tmp_path, vp, b"payload")
|
|
assert result.returncode == 1
|
|
assert "exactly one checksum= line" in result.stderr
|
|
|
|
|
|
@pytest.mark.parametrize("bad_tag", ["v0.19.0-beta;rm", "v0.19.0 beta", 'v0.19.0"beta'])
|
|
def test_render_refuses_values_outside_the_safe_set(tmp_path: Path, bad_tag: str) -> None:
|
|
vp = _scratch_void_packages(tmp_path)
|
|
result = _render(tmp_path, vp, b"payload", TAG=bad_tag)
|
|
assert result.returncode == 1
|
|
assert "outside [A-Za-z0-9._-]" in result.stderr
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Podman wiring: who may see the key, who may use the network
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_only_the_sign_phase_mounts_the_key() -> None:
|
|
"""Build scripts run in the build phase; they must never see the release key."""
|
|
text = _code(BUILD_RELEASE)
|
|
build = _body(text, "phase_build")
|
|
sign = _body(text, "phase_sign")
|
|
install = _body(text, "phase_install")
|
|
for name, body in (("build", build), ("install", install)):
|
|
assert "KEY_FILE" not in body, f"the {name} phase must not mount or name the key"
|
|
assert "/run/xbps-sign-key.pem" not in body
|
|
assert '-v "$KEY_FILE:/run/xbps-sign-key.pem:ro"' in sign, "the key is mounted read-only"
|
|
|
|
|
|
def test_the_sign_phase_has_no_network() -> None:
|
|
sign = _body(_code(BUILD_RELEASE), "phase_sign")
|
|
assert "--network none" in sign
|
|
|
|
|
|
def test_the_build_phase_is_the_only_one_that_runs_build_scripts() -> None:
|
|
text = _code(CONTAINER_RELEASE)
|
|
assert "xbps-src pkg" in _body_of_container(text, "phase_build")
|
|
for name in ("phase_sign", "phase_install"):
|
|
assert "xbps-src" not in _body_of_container(text, name), f"{name} must not build anything"
|
|
|
|
|
|
def _body_of_container(text: str, name: str) -> str:
|
|
match = re.search(rf"^{name}\(\) \{{\n(.*?)^\}}$", text, re.MULTILINE | re.DOTALL)
|
|
assert match, f"{name}() must be a function in container-release.sh"
|
|
return match.group(1)
|
|
|
|
|
|
def test_every_podman_run_uses_the_pinned_base_image() -> None:
|
|
text = _code(BUILD_RELEASE)
|
|
runs = text.count("podman run")
|
|
assert runs == 3, "build, sign and install: exactly three containers"
|
|
assert text.count('"$BASE_IMAGE" /bin/sh /mnt/container-release.sh') == 3
|
|
assert "ghcr.io" not in text, "the base image is pinned in tool_checksums.txt only"
|
|
|
|
|
|
@pytest.mark.parametrize("path", [BUILD_RELEASE, CONTAINER_RELEASE, RENDER, SMOKE_SH])
|
|
def test_no_docker_in_the_release_path(path: Path) -> None:
|
|
assert not re.search(r"\bdocker\b", _code(path), re.IGNORECASE), f"{path.name} names Docker"
|
|
|
|
|
|
def test_the_checks_run_before_the_sdist_and_before_any_container() -> None:
|
|
text = _code(BUILD_RELEASE)
|
|
pipeline = _body(text, "run_pipeline")
|
|
order = [
|
|
pipeline.index("check_template"),
|
|
pipeline.index("check_key"),
|
|
pipeline.index("build_sdist"),
|
|
pipeline.index("phase_build"),
|
|
]
|
|
assert order == sorted(order), order
|
|
|
|
|
|
def test_the_key_is_never_echoed_or_copied() -> None:
|
|
text = _code(BUILD_RELEASE)
|
|
assert "set -x" not in text
|
|
assert not re.search(r'cat\s+"?\$KEY_FILE', text)
|
|
assert not re.search(r'cp\s+"?\$KEY_FILE', text), "the key is mounted, never copied"
|
|
|
|
|
|
def test_the_build_tree_is_pinned_before_xbps_src_runs() -> None:
|
|
build = _body_of_container(_code(CONTAINER_RELEASE), "phase_build")
|
|
assert build.index("render-voculinux.sh") < build.index("xbps-src binary-bootstrap")
|
|
assert build.index("chown -R builder") < build.index("xbps-src binary-bootstrap")
|
|
|
|
|
|
def test_render_writes_only_inside_the_build_tree() -> None:
|
|
text = _code(RENDER)
|
|
assert 'TEMPLATE="$VP/srcpkgs/voculinux/template"' in text
|
|
assert 'DEST="$VP/hostdir/sources' in text
|
|
assert "$ROOT" not in text and "REPO" not in text, "the repository is never a target"
|
|
|
|
|
|
def test_the_install_phase_proves_the_key_is_needed() -> None:
|
|
install = _body_of_container(_code(CONTAINER_RELEASE), "phase_install")
|
|
assert "xbps-install -S --repository" in install, "the unsigned attempt runs without -y"
|
|
assert "refused without the key" in install
|
|
assert "/var/db/xbps/keys" in install
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The fingerprint helper: the pinned value is derived, not typed
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_the_committed_key_has_the_pinned_fingerprint(tmp_path: Path) -> None:
|
|
pem = tmp_path / "pub.pem"
|
|
pem.write_bytes(plistlib.loads(PLIST.read_bytes())["public-key"])
|
|
result = subprocess.run(
|
|
[sys.executable, "-I", str(FINGERPRINT_PY), "fingerprint", str(pem)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
check=True,
|
|
)
|
|
assert result.stdout.strip() == PINNED_FINGERPRINT
|
|
|
|
|
|
def test_the_plist_writer_round_trips_the_public_key(tmp_path: Path) -> None:
|
|
if OPENSSL is None:
|
|
pytest.skip("openssl is not installed")
|
|
key = _key(tmp_path / "key.pem", 2048)
|
|
pem = tmp_path / "pub.pem"
|
|
subprocess.run(
|
|
[OPENSSL, "rsa", "-in", str(key), "-pubout", "-out", str(pem)],
|
|
capture_output=True,
|
|
check=True,
|
|
timeout=60,
|
|
)
|
|
out = tmp_path / "client.plist"
|
|
subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-I",
|
|
str(FINGERPRINT_PY),
|
|
"plist",
|
|
str(pem),
|
|
"Test <t@example.com>",
|
|
str(out),
|
|
],
|
|
capture_output=True,
|
|
check=True,
|
|
timeout=60,
|
|
)
|
|
entry = plistlib.loads(out.read_bytes())
|
|
assert entry["public-key"] == pem.read_bytes()
|
|
assert entry["public-key-size"] == 2048
|
|
assert entry["signature-by"] == "Test <t@example.com>"
|
|
|
|
|
|
def test_smoke_is_the_same_path_with_a_throwaway_key() -> None:
|
|
"""The smoke gate delegates to the release path; it never names the release key."""
|
|
text = _code(SMOKE_SH)
|
|
assert 'exec "$HERE/build-release.sh" smoke' in text
|
|
assert "XBPS_SIGNING_KEY" not in text
|
|
release = _code(BUILD_RELEASE)
|
|
assert "throwaway.pem" in release and "genrsa" in release
|