"""Unit tests for the winimage subpackage (autounattend, iso_patch).""" from __future__ import annotations import subprocess from pathlib import Path from typing import Any import click import pytest from ci_orchestrator.winimage import autounattend as au from ci_orchestrator.winimage import iso_patch as ip from ci_orchestrator.winimage import massgrave as mg # ── massgrave resolver ─────────────────────────────────────────────────────── _SAMPLE_MD = """\ ## Windows Server 2025 Build - 26100.32860 (May 2026) | English | x64 | [en-us_windows_server_2025_updated_may_2026_x64_dvd_abc.iso](https://zerofs.link/f/AAA/)| | Italian | x64 | [it-it_windows_server_2025_updated_may_2026_x64_dvd_abc.iso](https://zerofs.link/f/BBB/)| Build - 26100.1742 | English | x64 | [X23-81958_26100.1742_en-us.iso](https://oemsoc.download.prss.microsoft.com/x.iso?t=z)| ## Windows Server 2022 | English | x64 | [en-us_windows_server_2022.iso](https://zerofs.link/f/CCC/)| """ def test_resolve_ws2025_picks_newest_english() -> None: i = mg.resolve_ws2025(_SAMPLE_MD, lang="English", arch="x64") assert i.build == "26100.32860" assert i.filename.startswith("en-us_windows_server_2025_updated") assert i.url == "https://zerofs.link/f/AAA/" def test_resolve_ws2025_italian() -> None: assert mg.resolve_ws2025(_SAMPLE_MD, lang="Italian").url == "https://zerofs.link/f/BBB/" def test_resolve_ws2025_not_found() -> None: with pytest.raises(click.ClickException, match="No Windows Server 2025"): mg.resolve_ws2025(_SAMPLE_MD, lang="Klingon") def test_ws2025_section_missing() -> None: with pytest.raises(click.ClickException, match="section not found"): mg._ws2025_section("## Windows Server 2022\nnothing here") # ── autounattend ─────────────────────────────────────────────────────────── def test_render_autounattend_no_placeholder() -> None: xml = au.render_autounattend(au.WinUnattendParams()) assert "{{" not in xml and "}}" not in xml assert "TVRH6-WHNXV-R9WG3-9XRFY-MY832" in xml def test_render_post_install_substitutes() -> None: ps = au.render_post_install(au.WinUnattendParams(admin_password="Secret123")) assert "Secret123" in ps and "{{" not in ps assert "install_complete.flag" in ps def test_validate_computer_name_ok() -> None: au.validate_computer_name("WINBUILD-2025") @pytest.mark.parametrize("bad", ["bad name", "toolongcomputername01", "has_underscore"]) def test_validate_computer_name_bad(bad: str) -> None: with pytest.raises(ValueError, match="invalid computer name"): au.validate_computer_name(bad) def test_apply_detects_leftover() -> None: with pytest.raises(ValueError, match="unsubstituted placeholder"): au._apply("hello {{MISSING}}", {}) # ── iso_patch ────────────────────────────────────────────────────────────── def test_read_volid_fallback(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: monkeypatch.setattr(ip.shutil, "which", lambda _n: None) iso = tmp_path / "x.iso" iso.write_bytes(b"x") assert ip._read_volid(iso) == "WINSERVER" def test_check_free_space_raises(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: iso = tmp_path / "x.iso" iso.write_bytes(b"x" * 1000) monkeypatch.setattr( ip.shutil, "disk_usage", lambda _p: type("DU", (), {"free": 10})(), ) with pytest.raises(click.ClickException, match="insufficient space"): ip._check_free_space(iso, tmp_path) def test_patch_iso_happy(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: base = tmp_path / "base.iso" base.write_bytes(b"x" * 1000) out = tmp_path / "out.iso" monkeypatch.setattr(ip, "_require_xorriso", lambda: "xorriso") monkeypatch.setattr(ip, "_require_7z", lambda: "7z") monkeypatch.setattr(ip, "_read_volid", lambda _p: "WINSERVER") monkeypatch.setattr(ip, "_check_free_space", lambda *_a: None) def fake_run(argv: list[str], **_kw: Any) -> subprocess.CompletedProcess[str]: # simulate 7z extraction creating the efi boot file in the tree if argv[0] == "7z" and "x" in argv: outflag = next(a for a in argv if a.startswith("-o")) tree = Path(outflag[2:]) boot = tree / ip._EFI_BOOT_NOPROMPT boot.parent.mkdir(parents=True, exist_ok=True) boot.write_bytes(b"boot") elif "mkisofs" in argv: out.write_bytes(b"iso") return subprocess.CompletedProcess(argv, 0, "", "") monkeypatch.setattr(ip.subprocess, "run", fake_run) result = ip.patch_iso( base, autounattend_xml="", post_install_ps1="echo hi", out_iso=out, work_dir=tmp_path, ) assert result == out and out.is_file() def test_iso_has_root_file(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: monkeypatch.setattr(ip, "_require_7z", lambda: "7z") iso = tmp_path / "x.iso" iso.write_bytes(b"x") def fake_run(argv: list[str], **_k: Any) -> subprocess.CompletedProcess[str]: out = "2026 .... 6813 autounattend.xml\n" if "-i!autounattend.xml" in argv else "" return subprocess.CompletedProcess(argv, 0, out, "") monkeypatch.setattr(ip.subprocess, "run", fake_run) assert ip.iso_has_root_file(iso, "autounattend.xml") is True assert ip.iso_has_root_file(iso, "missing.xml") is False def test_patch_iso_missing_efi_boot(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: base = tmp_path / "base.iso" base.write_bytes(b"x" * 1000) monkeypatch.setattr(ip, "_require_xorriso", lambda: "xorriso") monkeypatch.setattr(ip, "_require_7z", lambda: "7z") monkeypatch.setattr(ip, "_check_free_space", lambda *_a: None) monkeypatch.setattr( ip.subprocess, "run", lambda argv, **_k: subprocess.CompletedProcess(argv, 0, "", ""), ) with pytest.raises(click.ClickException, match="no-prompt"): ip.patch_iso( base, autounattend_xml="", post_install_ps1="x", out_iso=tmp_path / "out.iso", work_dir=tmp_path, )