IQ.Pilot Prebuilt Release @ 27f668a
This commit is contained in:
216
artifacts/runtime/test_apply_boot_branding.py
Normal file
216
artifacts/runtime/test_apply_boot_branding.py
Normal file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from artifacts.runtime import apply_boot_branding as abb
|
||||
|
||||
|
||||
COMMA_HOME = "/home/comma"
|
||||
|
||||
EXISTING_PASSWD = (
|
||||
"root:x:0:0:root:/root:/bin/bash\n"
|
||||
"daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n"
|
||||
f"comma:x:1000:1000:comma:{COMMA_HOME}:/bin/bash\n"
|
||||
"nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n"
|
||||
)
|
||||
|
||||
EXISTING_SHADOW = (
|
||||
"root:*:19000:0:99999:7:::\n"
|
||||
"daemon:*:19000:0:99999:7:::\n"
|
||||
"comma:$6$abc$xyz:19000:0:99999:7:::\n"
|
||||
"nobody:!:19000:0:99999:7:::\n"
|
||||
)
|
||||
|
||||
EXPECTED_PASSWD_LINE = f"iq:x:1000:1000:IQ.Pilot:{COMMA_HOME}:/bin/bash"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def etc(tmp_path: Path) -> tuple[Path, Path]:
|
||||
passwd = tmp_path / "passwd"
|
||||
shadow = tmp_path / "shadow"
|
||||
passwd.write_text(EXISTING_PASSWD, encoding="utf-8")
|
||||
shadow.write_text(EXISTING_SHADOW, encoding="utf-8")
|
||||
return passwd, shadow
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_remount():
|
||||
calls: list[str] = []
|
||||
|
||||
def _remount(mode: str) -> None:
|
||||
calls.append(mode)
|
||||
|
||||
_remount.calls = calls
|
||||
return _remount
|
||||
|
||||
|
||||
def _frozen_now() -> float:
|
||||
return 1779494400.0
|
||||
|
||||
|
||||
def test_fresh_install_adds_iq_user(etc, fake_remount):
|
||||
passwd, shadow = etc
|
||||
|
||||
changed = abb.ensure_iq_user(
|
||||
passwd_path=passwd,
|
||||
shadow_path=shadow,
|
||||
remount=fake_remount,
|
||||
root_opts="ro,relatime",
|
||||
now=_frozen_now,
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
passwd_text = passwd.read_text(encoding="utf-8")
|
||||
shadow_text = shadow.read_text(encoding="utf-8")
|
||||
|
||||
assert EXPECTED_PASSWD_LINE in passwd_text.splitlines()
|
||||
assert any(l.startswith("iq:!:") and l.endswith(":0:99999:7:::") for l in shadow_text.splitlines())
|
||||
|
||||
for original_line in EXISTING_PASSWD.splitlines():
|
||||
assert original_line in passwd_text.splitlines()
|
||||
for original_line in EXISTING_SHADOW.splitlines():
|
||||
assert original_line in shadow_text.splitlines()
|
||||
|
||||
assert fake_remount.calls == ["remount,rw", "remount,ro,relatime"]
|
||||
|
||||
|
||||
def test_second_boot_is_noop(etc, fake_remount):
|
||||
passwd, shadow = etc
|
||||
|
||||
abb.ensure_iq_user(passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now)
|
||||
passwd_after_first = passwd.read_text(encoding="utf-8")
|
||||
shadow_after_first = shadow.read_text(encoding="utf-8")
|
||||
fake_remount.calls.clear()
|
||||
|
||||
changed = abb.ensure_iq_user(
|
||||
passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now,
|
||||
)
|
||||
|
||||
assert changed is False
|
||||
assert passwd.read_text(encoding="utf-8") == passwd_after_first
|
||||
assert shadow.read_text(encoding="utf-8") == shadow_after_first
|
||||
assert fake_remount.calls == [] # no remount when nothing to do
|
||||
|
||||
|
||||
def test_partial_state_self_heals(etc, fake_remount):
|
||||
passwd, shadow = etc
|
||||
passwd.write_text(EXISTING_PASSWD + EXPECTED_PASSWD_LINE + "\n", encoding="utf-8")
|
||||
# shadow lacks iq
|
||||
|
||||
changed = abb.ensure_iq_user(
|
||||
passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now,
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
iq_passwd_lines = [l for l in passwd.read_text(encoding="utf-8").splitlines() if l.startswith("iq:")]
|
||||
iq_shadow_lines = [l for l in shadow.read_text(encoding="utf-8").splitlines() if l.startswith("iq:")]
|
||||
assert iq_passwd_lines == [EXPECTED_PASSWD_LINE] # not duplicated
|
||||
assert len(iq_shadow_lines) == 1
|
||||
assert iq_shadow_lines[0].startswith("iq:!:")
|
||||
|
||||
|
||||
def test_malformed_iq_passwd_line_replaced(etc, fake_remount):
|
||||
passwd, shadow = etc
|
||||
wrong = "iq:x:1000:1000:old:/root:/bin/sh"
|
||||
passwd.write_text(EXISTING_PASSWD + wrong + "\n", encoding="utf-8")
|
||||
|
||||
changed = abb.ensure_iq_user(
|
||||
passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now,
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
iq_lines = [l for l in passwd.read_text(encoding="utf-8").splitlines() if l.startswith("iq:")]
|
||||
assert iq_lines == [EXPECTED_PASSWD_LINE]
|
||||
|
||||
|
||||
def test_remount_ro_restored_on_write_failure(tmp_path, fake_remount):
|
||||
passwd = tmp_path / "ro_passwd"
|
||||
passwd.write_text(EXISTING_PASSWD, encoding="utf-8")
|
||||
passwd.chmod(0o444) # force write to raise
|
||||
|
||||
shadow = tmp_path / "shadow"
|
||||
shadow.write_text(EXISTING_SHADOW, encoding="utf-8")
|
||||
|
||||
with pytest.raises(PermissionError):
|
||||
abb.ensure_iq_user(
|
||||
passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now,
|
||||
)
|
||||
|
||||
assert fake_remount.calls == ["remount,rw", "remount,ro"]
|
||||
|
||||
|
||||
def test_does_not_alter_existing_branding_flow(monkeypatch, etc, fake_remount, capsys):
|
||||
passwd, shadow = etc
|
||||
|
||||
monkeypatch.setattr(abb, "PASSWD_PATH", passwd)
|
||||
monkeypatch.setattr(abb, "SHADOW_PATH", shadow)
|
||||
monkeypatch.setattr(abb, "remount_root", fake_remount)
|
||||
monkeypatch.setattr(abb, "get_root_mount_options", lambda: "ro,relatime")
|
||||
monkeypatch.setattr(abb.os, "geteuid", lambda: 0)
|
||||
monkeypatch.setattr(abb, "apply_branding", lambda: False) # no assets → branding no-op
|
||||
monkeypatch.setattr("sys.argv", ["apply_boot_branding.py"])
|
||||
|
||||
rc = abb.main()
|
||||
out = capsys.readouterr().out.strip().splitlines()
|
||||
|
||||
assert rc == 0
|
||||
assert out[-1] == "changed" # iq_changed flips combined result
|
||||
assert EXPECTED_PASSWD_LINE in passwd.read_text(encoding="utf-8").splitlines()
|
||||
|
||||
|
||||
def test_iq_home_tracks_commas_actual_home(tmp_path, fake_remount):
|
||||
passwd = tmp_path / "passwd"
|
||||
shadow = tmp_path / "shadow"
|
||||
unusual_home = "/data/some/relocated/home/comma"
|
||||
passwd.write_text(
|
||||
"root:x:0:0:root:/root:/bin/bash\n"
|
||||
f"comma:x:1000:1000:comma:{unusual_home}:/bin/zsh\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
shadow.write_text("comma:$6$x$y:19000:0:99999:7:::\n", encoding="utf-8")
|
||||
|
||||
changed = abb.ensure_iq_user(
|
||||
passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now,
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
iq_line = [l for l in passwd.read_text(encoding="utf-8").splitlines() if l.startswith("iq:")][0]
|
||||
# iq inherits comma's home, shell, UID, GID — not a hardcoded path
|
||||
assert iq_line == f"iq:x:1000:1000:IQ.Pilot:{unusual_home}:/bin/zsh"
|
||||
|
||||
|
||||
def test_skips_when_comma_user_absent(tmp_path, fake_remount):
|
||||
passwd = tmp_path / "passwd"
|
||||
shadow = tmp_path / "shadow"
|
||||
passwd.write_text("root:x:0:0:root:/root:/bin/bash\n", encoding="utf-8")
|
||||
shadow.write_text("root:*:19000:0:99999:7:::\n", encoding="utf-8")
|
||||
|
||||
changed = abb.ensure_iq_user(
|
||||
passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now,
|
||||
)
|
||||
|
||||
assert changed is False
|
||||
assert "iq:" not in passwd.read_text(encoding="utf-8")
|
||||
assert "iq:" not in shadow.read_text(encoding="utf-8")
|
||||
assert fake_remount.calls == []
|
||||
|
||||
|
||||
def test_ensure_iq_user_failure_is_swallowed_by_main(monkeypatch, capsys):
|
||||
def boom(*a, **kw):
|
||||
raise RuntimeError("simulated failure")
|
||||
|
||||
monkeypatch.setattr(abb, "ensure_iq_user", boom)
|
||||
monkeypatch.setattr(abb.os, "geteuid", lambda: 0)
|
||||
monkeypatch.setattr(abb, "apply_branding", lambda: True)
|
||||
monkeypatch.setattr("sys.argv", ["apply_boot_branding.py"])
|
||||
|
||||
rc = abb.main()
|
||||
out = capsys.readouterr().out
|
||||
|
||||
assert rc == 0
|
||||
assert "ensure_iq_user failed: simulated failure" in out
|
||||
assert "changed" in out # branding ran, so combined result is still "changed"
|
||||
Reference in New Issue
Block a user