from app import ( build_archinstall_config, build_archinstall_creds, validate_step1, ) def test_validate_step1_accepts_good_input(): errors, values = validate_step1( { "hostname": "furtka", "username": "daniel", "password": "topsecretpw", "password2": "topsecretpw", "language": "de", } ) assert errors == [] assert values == { "hostname": "furtka", "username": "daniel", "password": "topsecretpw", "language": "de", } def test_validate_step1_collects_all_errors(): errors, _ = validate_step1( { "hostname": "BAD!", "username": "1bad", "password": "short", "password2": "mismatch", "language": "xx", } ) assert len(errors) == 5 def test_build_archinstall_config_uses_selected_locale(monkeypatch): # build_disk_config imports archinstall lazily; archinstall isn't # installed in CI (only runs on the live ISO), so stub it out. import app as app_module monkeypatch.setattr(app_module, "build_disk_config", lambda d: {"stubbed_device": d}) cfg = build_archinstall_config( { "hostname": "h", "username": "u", "password": "pw12345678", "language": "pl", "boot_drive": "/dev/sda", } ) assert cfg["disk_config"] == {"stubbed_device": "/dev/sda"} assert cfg["hostname"] == "h" assert cfg["locale_config"]["locale"] == "pl_PL.UTF-8" # Users moved out of config into creds once we adopted archinstall 4.x's # `!password` sentinel; config only carries a gpasswd in custom_commands # so the user lands in the docker group after docker is pacstrapped. assert "users" not in cfg assert cfg["custom_commands"][0] == "gpasswd -a u docker" def test_build_archinstall_config_includes_post_install_bootstrap(monkeypatch, tmp_path): # The installed system should come up with a Furtka landing page at # http://.local. That means caddy + avahi pacstrapped, the # matching services enabled, a Caddyfile written into the target rootfs, # nss-mdns spliced into nsswitch.conf, and the resource-manager tarball # extracted into the versioned /opt/furtka/current layout. import app as app_module # Fake payload so _resource_manager_commands emits its full cmd tree. fake_payload = tmp_path / "payload.tar.gz" fake_payload.write_bytes(b"\x1f\x8b\x08\x00fake") monkeypatch.setattr(app_module, "RESOURCE_MANAGER_PAYLOAD", fake_payload) monkeypatch.setattr(app_module, "build_disk_config", lambda d: {"stubbed_device": d}) cfg = build_archinstall_config( { "hostname": "heimserver", "username": "u", "password": "pw12345678", "language": "en", "boot_drive": "/dev/sda", } ) for pkg in ("caddy", "avahi", "nss-mdns"): assert pkg in cfg["packages"] # Packaged units go in `services` (enabled before custom_commands runs); # our own units don't exist at that point, so they must be enabled from # within custom_commands after the unit files land on disk. for svc in ("caddy", "avahi-daemon"): assert svc in cfg["services"] assert "furtka-welcome" not in cfg["services"] assert "furtka-status.timer" not in cfg["services"] joined = "\n".join(cfg["custom_commands"]) # Every furtka-* unit is systemctl-linked from the shipped asset tree and # then enabled — no hand-written files under /etc/systemd/system/. assert "systemctl link /opt/furtka/current/assets/systemd/" in joined assert "systemctl enable furtka-api.service" in joined for path in ( "/etc/caddy/Caddyfile", "/var/lib/furtka/status.json", "/var/lib/furtka/furtka.json", ): assert path in joined, f"expected {path} to be written by custom_commands" # /srv/furtka/www/ is retired — no writes should target it anymore. assert "/srv/furtka/www" not in joined assert "mdns_minimal" in joined assert "nsswitch.conf" in joined # Hostname is injected via /var/lib/furtka/furtka.json, not via sed. assert "__HOSTNAME__" not in joined def test_resource_manager_payload_landed_when_present(monkeypatch, tmp_path): # When iso/build.sh has staged the resource-manager tarball, the # post-install commands should untar it into /opt/furtka/versions//, # flip the /opt/furtka/current symlink, drop the `furtka` wrapper, and # systemctl-link every Furtka unit file. import app as app_module fake_payload = tmp_path / "furtka-resource-manager.tar.gz" fake_payload.write_bytes(b"\x1f\x8b\x08\x00fake-tarball-bytes") monkeypatch.setattr(app_module, "RESOURCE_MANAGER_PAYLOAD", fake_payload) monkeypatch.setattr(app_module, "build_disk_config", lambda d: {"stubbed_device": d}) cfg = build_archinstall_config( { "hostname": "heimserver", "username": "u", "password": "pw12345678", "language": "en", "boot_drive": "/dev/sda", } ) joined = "\n".join(cfg["custom_commands"]) # Tarball lands in the versioned slot, not flat /opt/furtka/. assert 'tar -xzf - -C "$staging"' in joined assert "/opt/furtka/versions/$ver" in joined assert 'ln -sfn "/opt/furtka/versions/$ver" /opt/furtka/current' in joined # CLI wrapper lands and references /opt/furtka/current. The wrapper body # rides the base64 payload, so assert on the constant directly. assert "/usr/local/bin/furtka" in joined assert "PYTHONPATH=/opt/furtka/current" in app_module._FURTKA_WRAPPER_SH # Every unit is linked + enabled. for unit in app_module._FURTKA_UNITS: assert f"/opt/furtka/current/assets/systemd/{unit}" in joined assert unit in joined # python is pacstrapped so the wrapper has an interpreter. assert "python" in cfg["packages"] def test_resource_manager_absent_without_payload(monkeypatch, tmp_path): # Dev box / CI without an ISO build: payload doesn't exist. No tarball # extract, no unit link, no enable — and no stale references to furtka-* # units in custom_commands. import app as app_module monkeypatch.setattr(app_module, "RESOURCE_MANAGER_PAYLOAD", tmp_path / "does-not-exist.tar.gz") monkeypatch.setattr(app_module, "build_disk_config", lambda d: {"stubbed_device": d}) cfg = build_archinstall_config( { "hostname": "heimserver", "username": "u", "password": "pw12345678", "language": "en", "boot_drive": "/dev/sda", } ) joined = "\n".join(cfg["custom_commands"]) assert "tar -xzf" not in joined assert "systemctl link" not in joined # The base system bootstrap (caddy, status.json placeholder, furtka.json) # is unaffected. assert "/etc/caddy/Caddyfile" in joined assert "/var/lib/furtka/furtka.json" in joined def test_build_archinstall_creds_uses_archinstall_sentinel_keys(): creds = build_archinstall_creds({"username": "u", "password": "pw12345678"}) assert creds["!root-password"] == "pw12345678" assert creds["users"] == [ { "username": "u", "!password": "pw12345678", "sudo": True, "groups": [], } ]