aboutsummaryrefslogtreecommitdiff
path: root/tests/unit
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-04-26 01:09:01 -0500
committerCraig Jennings <c@cjennings.net>2026-04-26 01:09:01 -0500
commit1a261b0c220903c8bb628e7f2b94cf75a843f688 (patch)
treea4d6d7db9148bb450738818ef59755a5e2eebee3 /tests/unit
parent6b65665eca8a4b36b0b6eae4d761fccd7b4c1fc4 (diff)
downloadarchangel-1a261b0c220903c8bb628e7f2b94cf75a843f688.tar.gz
archangel-1a261b0c220903c8bb628e7f2b94cf75a843f688.zip
test: expand bats coverage across installer modules
Added unit tests for `disk.sh`, `btrfs.sh`, the archangel monolith's `gather_input` unattended branch, and filled gap cases in `config.sh`. The suite grew from 71 to 110 tests. `installer/lib/disk.sh` was completely uncovered. New `tests/unit/test_disk.bats` covers the four pure partition-path helpers (`get_efi_partition`, `get_root_partition`, `get_efi_partitions`, `get_root_partitions`) across SATA, virtio, and NVMe inputs, mixed arrays, and the empty-input behavior. Side-effecting functions in the same file (sgdisk, mkfs.fat, partprobe, and fzf wrappers) stay deliberately VM-tested. `installer/lib/btrfs.sh` had no bats coverage. New `tests/unit/test_btrfs.bats` covers `get_luks_devices`, the only pure helper in the file. It pins the asymmetric naming convention where the first device gets the bare `LUKS_MAPPER_NAME` and subsequent devices append the index. The archangel monolith was un-source-able for tests because its top-level code created a /tmp log file and redirected stdout via `exec > >(tee...)`, plus called `main "$@"` unconditionally at the bottom. I extracted the logging setup into an `init_logging` function called from `main`, and wrapped the main call in a `[[ "${BASH_SOURCE[0]}" == "${0}" ]]` guard. Sourcing the script now loads function definitions silently, with no log file and no banner. Running it directly works exactly as before. Verified both paths. That refactor unlocks `tests/unit/test_archangel.bats`, which covers `gather_input` in unattended mode. Required-field validation for HOSTNAME, TIMEZONE, ROOT_PASSWORD, and DISKS. Optional-field defaulting (FILESYSTEM to zfs, LOCALE to en_US.UTF-8, KEYMAP to us, ENABLE_SSH to yes). Filesystem-specific encryption checks (ZFS_PASSPHRASE required when not NO_ENCRYPT, same for LUKS_PASSPHRASE on Btrfs). Filesystem validity. RAID_LEVEL defaulting for multi-disk installs. The interactive branch stays out of scope per the testing-strategy policy. `tests/unit/test_config.bats` got five gap tests: `check_config` when CONFIG_FILE is set, `validate_config` against a non-block-device entry (e.g. /dev/null) and a missing path, and `parse_args` accepting `--color` and `--config-file` together in either order. `testing-strategy.org` got an expanded "What bats does NOT cover" section. The doc previously named six tools (mkfs, cryptsetup, zpool create, pacstrap, arch-chroot, grub-install). The new list adds sgdisk, partprobe, blkid, mkfs.fat, mkfs.btrfs, snapper, efibootmgr, mount, umount, findmnt, mountpoint, and fzf. It also names the conditions (root needed, real /dev or /sys state) that make a function VM-only. The coverage table at the top now lists the three new test files. No behavior change in production code. The init_logging extraction preserves the existing log path and banner format byte-for-byte.
Diffstat (limited to 'tests/unit')
-rw-r--r--tests/unit/test_archangel.bats214
-rw-r--r--tests/unit/test_btrfs.bats56
-rw-r--r--tests/unit/test_config.bats47
-rw-r--r--tests/unit/test_disk.bats132
4 files changed, 449 insertions, 0 deletions
diff --git a/tests/unit/test_archangel.bats b/tests/unit/test_archangel.bats
new file mode 100644
index 0000000..c31cebb
--- /dev/null
+++ b/tests/unit/test_archangel.bats
@@ -0,0 +1,214 @@
+#!/usr/bin/env bats
+# Unit tests for the installer/archangel monolith.
+#
+# Coverage scope: gather_input() in unattended mode — the validation
+# of required config values, defaulting of optional ones, and the
+# filesystem-specific encryption checks. The interactive branch
+# (everything reachable via `if [[ "$UNATTENDED" != true ]]`) is not
+# unit-tested per the project's testing-strategy.org policy on
+# fzf / arch-chroot / mkfs / cryptsetup wrappers.
+#
+# Sourcing archangel relies on the source-guard at the bottom of
+# the script: when sourced, function definitions load but main is
+# not called, init_logging is not run (so /tmp/archangel-*.log is
+# not created), and the banner is not printed.
+
+setup() {
+ # shellcheck disable=SC1091
+ source "${BATS_TEST_DIRNAME}/../../installer/archangel"
+ UNATTENDED=true
+}
+
+#############################
+# Required-field validation
+#############################
+
+@test "gather_input unattended errors when HOSTNAME is missing" {
+ HOSTNAME=""
+ TIMEZONE=UTC
+ ROOT_PASSWORD=secret
+ SELECTED_DISKS=(/dev/sda)
+ NO_ENCRYPT=yes
+ run gather_input
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"HOSTNAME"* ]]
+}
+
+@test "gather_input unattended errors when TIMEZONE is missing" {
+ HOSTNAME=h
+ TIMEZONE=""
+ ROOT_PASSWORD=secret
+ SELECTED_DISKS=(/dev/sda)
+ NO_ENCRYPT=yes
+ run gather_input
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"TIMEZONE"* ]]
+}
+
+@test "gather_input unattended errors when ROOT_PASSWORD is missing" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=""
+ SELECTED_DISKS=(/dev/sda)
+ NO_ENCRYPT=yes
+ run gather_input
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"ROOT_PASSWORD"* ]]
+}
+
+@test "gather_input unattended errors when SELECTED_DISKS is empty" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=secret
+ SELECTED_DISKS=()
+ NO_ENCRYPT=yes
+ run gather_input
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"DISKS"* ]]
+}
+
+#############################
+# Optional-field defaults
+#############################
+
+@test "gather_input unattended defaults FILESYSTEM to zfs when empty" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda)
+ FILESYSTEM=""
+ NO_ENCRYPT=yes
+ gather_input >/dev/null
+ [ "$FILESYSTEM" = "zfs" ]
+}
+
+@test "gather_input unattended defaults LOCALE, KEYMAP, ENABLE_SSH when empty" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda)
+ FILESYSTEM=zfs
+ NO_ENCRYPT=yes
+ LOCALE=""
+ KEYMAP=""
+ ENABLE_SSH=""
+ gather_input >/dev/null
+ [ "$LOCALE" = "en_US.UTF-8" ]
+ [ "$KEYMAP" = "us" ]
+ [ "$ENABLE_SSH" = "yes" ]
+}
+
+@test "gather_input unattended preserves explicit non-default values" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda)
+ FILESYSTEM=btrfs
+ NO_ENCRYPT=yes
+ LOCALE="en_GB.UTF-8"
+ KEYMAP="dvorak"
+ ENABLE_SSH="no"
+ gather_input >/dev/null
+ [ "$FILESYSTEM" = "btrfs" ]
+ [ "$LOCALE" = "en_GB.UTF-8" ]
+ [ "$KEYMAP" = "dvorak" ]
+ [ "$ENABLE_SSH" = "no" ]
+}
+
+#############################
+# Filesystem-specific encryption validation
+#############################
+
+@test "gather_input unattended errors when ZFS without ZFS_PASSPHRASE and encryption on" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda)
+ FILESYSTEM=zfs
+ NO_ENCRYPT=no
+ ZFS_PASSPHRASE=""
+ run gather_input
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"ZFS_PASSPHRASE"* ]]
+}
+
+@test "gather_input unattended errors when Btrfs without LUKS_PASSPHRASE and encryption on" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda)
+ FILESYSTEM=btrfs
+ NO_ENCRYPT=no
+ LUKS_PASSPHRASE=""
+ run gather_input
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"LUKS_PASSPHRASE"* ]]
+}
+
+@test "gather_input unattended accepts ZFS with NO_ENCRYPT=yes and no passphrase" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda)
+ FILESYSTEM=zfs
+ NO_ENCRYPT=yes
+ ZFS_PASSPHRASE=""
+ run gather_input
+ [ "$status" -eq 0 ]
+}
+
+#############################
+# Filesystem validity
+#############################
+
+@test "gather_input unattended errors when FILESYSTEM is neither zfs nor btrfs" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda)
+ FILESYSTEM=ext4
+ NO_ENCRYPT=yes
+ run gather_input
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"Invalid FILESYSTEM"* ]]
+}
+
+#############################
+# RAID-level defaulting
+#############################
+
+@test "gather_input unattended defaults RAID_LEVEL to mirror for multi-disk install" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda /dev/sdb)
+ FILESYSTEM=zfs
+ NO_ENCRYPT=yes
+ RAID_LEVEL=""
+ gather_input >/dev/null
+ [ "$RAID_LEVEL" = "mirror" ]
+}
+
+@test "gather_input unattended preserves an explicit RAID_LEVEL on multi-disk install" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda /dev/sdb /dev/sdc)
+ FILESYSTEM=zfs
+ NO_ENCRYPT=yes
+ RAID_LEVEL="raidz1"
+ gather_input >/dev/null
+ [ "$RAID_LEVEL" = "raidz1" ]
+}
+
+@test "gather_input unattended leaves RAID_LEVEL empty for single-disk install" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/sda)
+ FILESYSTEM=zfs
+ NO_ENCRYPT=yes
+ RAID_LEVEL=""
+ gather_input >/dev/null
+ [ -z "$RAID_LEVEL" ]
+}
diff --git a/tests/unit/test_btrfs.bats b/tests/unit/test_btrfs.bats
new file mode 100644
index 0000000..890bba2
--- /dev/null
+++ b/tests/unit/test_btrfs.bats
@@ -0,0 +1,56 @@
+#!/usr/bin/env bats
+# Unit tests for installer/lib/btrfs.sh
+#
+# Coverage scope: pure helpers only. Most of btrfs.sh wraps cryptsetup,
+# mkfs.btrfs, snapper, grub-install, and arch-chroot — all deliberately
+# VM-tested per the project's testing-strategy.org policy. Only
+# get_luks_devices is covered here.
+
+setup() {
+ # shellcheck disable=SC1091
+ source "${BATS_TEST_DIRNAME}/../../installer/lib/common.sh"
+ # shellcheck disable=SC1091
+ source "${BATS_TEST_DIRNAME}/../../installer/lib/btrfs.sh"
+}
+
+#############################
+# get_luks_devices
+#############################
+# Asymmetric naming: index 0 uses the bare LUKS_MAPPER_NAME (no
+# suffix), subsequent indices append the index. Tests pin both the
+# bare-first behavior and the suffix-on-rest behavior.
+
+@test "get_luks_devices: count=1 emits the bare-named mapper device" {
+ LUKS_MAPPER_NAME="cryptroot"
+ run get_luks_devices 1
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/mapper/cryptroot" ]
+}
+
+@test "get_luks_devices: count=3 emits bare name + suffixed entries" {
+ LUKS_MAPPER_NAME="cryptroot"
+ run get_luks_devices 3
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/mapper/cryptroot /dev/mapper/cryptroot1 /dev/mapper/cryptroot2" ]
+}
+
+@test "get_luks_devices: count=0 emits empty output" {
+ LUKS_MAPPER_NAME="cryptroot"
+ run get_luks_devices 0
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
+
+@test "get_luks_devices: count=5 emits five entries with correct suffix progression" {
+ LUKS_MAPPER_NAME="cryptroot"
+ run get_luks_devices 5
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/mapper/cryptroot /dev/mapper/cryptroot1 /dev/mapper/cryptroot2 /dev/mapper/cryptroot3 /dev/mapper/cryptroot4" ]
+}
+
+@test "get_luks_devices: non-numeric count is treated as zero (no crash)" {
+ LUKS_MAPPER_NAME="cryptroot"
+ run get_luks_devices abc
+ [ "$status" -eq 0 ]
+ [ -z "$output" ]
+}
diff --git a/tests/unit/test_config.bats b/tests/unit/test_config.bats
index c659fdc..c0c4e42 100644
--- a/tests/unit/test_config.bats
+++ b/tests/unit/test_config.bats
@@ -109,3 +109,50 @@ EOF
[ "$status" -eq 0 ]
[ -z "$output" ]
}
+
+@test "check_config loads the config file when CONFIG_FILE is set" {
+ local tmp
+ tmp=$(mktemp)
+ cat >"$tmp" <<'EOF'
+HOSTNAME=fromcheckconfig
+TIMEZONE=UTC
+EOF
+ CONFIG_FILE="$tmp"
+ check_config
+ [ "$HOSTNAME" = "fromcheckconfig" ]
+ [ "$TIMEZONE" = "UTC" ]
+ [ "$UNATTENDED" = "true" ]
+ rm -f "$tmp"
+}
+
+@test "validate_config flags an existing-but-not-block path in SELECTED_DISKS" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=x
+ SELECTED_DISKS=(/dev/null)
+ run validate_config
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"Disk not found: /dev/null"* ]]
+}
+
+@test "validate_config flags a missing path in SELECTED_DISKS" {
+ HOSTNAME=h
+ TIMEZONE=UTC
+ ROOT_PASSWORD=x
+ SELECTED_DISKS=(/nonexistent/disk-xyz-42)
+ run validate_config
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"Disk not found"* ]]
+}
+
+@test "parse_args accepts --color and --config-file together (color first)" {
+ parse_args --color --config-file /tmp/foo.conf
+ [ "$CONFIG_FILE" = "/tmp/foo.conf" ]
+ [ -n "$RED" ]
+}
+
+@test "parse_args accepts --config-file and --color together (config first)" {
+ parse_args --config-file /tmp/foo.conf --color
+ [ "$CONFIG_FILE" = "/tmp/foo.conf" ]
+ [ -n "$RED" ]
+}
diff --git a/tests/unit/test_disk.bats b/tests/unit/test_disk.bats
new file mode 100644
index 0000000..f4e6929
--- /dev/null
+++ b/tests/unit/test_disk.bats
@@ -0,0 +1,132 @@
+#!/usr/bin/env bats
+# Unit tests for installer/lib/disk.sh
+#
+# Coverage scope: pure partition-path helpers only. Side-effecting
+# functions (partition_disk, partition_disks, format_efi,
+# format_efi_partitions, select_disks) wrap sgdisk / mkfs.fat /
+# partprobe / fzf and are validated by VM integration per the
+# project's testing-strategy.org policy.
+
+setup() {
+ # shellcheck disable=SC1091
+ source "${BATS_TEST_DIRNAME}/../../installer/lib/common.sh"
+ # shellcheck disable=SC1091
+ source "${BATS_TEST_DIRNAME}/../../installer/lib/disk.sh"
+}
+
+#############################
+# get_efi_partition
+#############################
+
+@test "get_efi_partition: SATA disk gets numeric suffix 1" {
+ run get_efi_partition /dev/sda
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/sda1" ]
+}
+
+@test "get_efi_partition: virtio disk gets numeric suffix 1" {
+ run get_efi_partition /dev/vda
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/vda1" ]
+}
+
+@test "get_efi_partition: NVMe disk gets p1 suffix" {
+ run get_efi_partition /dev/nvme0n1
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/nvme0n1p1" ]
+}
+
+@test "get_efi_partition: NVMe with multi-namespace disk gets p1 suffix" {
+ run get_efi_partition /dev/nvme1n2
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/nvme1n2p1" ]
+}
+
+@test "get_efi_partition: empty input documents current behavior" {
+ # Empty input misses the nvme regex so the bare-suffix branch fires,
+ # producing just "1". This pins the existing behavior; the function
+ # is never called with empty in production but pinning catches a
+ # change in suffix-rule logic.
+ run get_efi_partition ""
+ [ "$status" -eq 0 ]
+ [ "$output" = "1" ]
+}
+
+#############################
+# get_root_partition
+#############################
+
+@test "get_root_partition: SATA disk gets numeric suffix 2" {
+ run get_root_partition /dev/sda
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/sda2" ]
+}
+
+@test "get_root_partition: NVMe disk gets p2 suffix" {
+ run get_root_partition /dev/nvme0n1
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/nvme0n1p2" ]
+}
+
+@test "get_root_partition: virtio disk gets numeric suffix 2" {
+ run get_root_partition /dev/vdb
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/vdb2" ]
+}
+
+#############################
+# get_efi_partitions
+#############################
+
+@test "get_efi_partitions: two SATA disks emit two suffixed partitions" {
+ run get_efi_partitions /dev/sda /dev/sdb
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/sda1
+/dev/sdb1" ]
+}
+
+@test "get_efi_partitions: mixed SATA + NVMe gets correct per-disk suffix" {
+ run get_efi_partitions /dev/sda /dev/nvme0n1
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/sda1
+/dev/nvme0n1p1" ]
+}
+
+@test "get_efi_partitions: single NVMe disk emits one p1 partition" {
+ run get_efi_partitions /dev/nvme0n1
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/nvme0n1p1" ]
+}
+
+@test "get_efi_partitions: three disks emit three lines in order" {
+ run get_efi_partitions /dev/sda /dev/sdb /dev/sdc
+ [ "$status" -eq 0 ]
+ local lines
+ lines=$(echo "$output" | wc -l)
+ [ "$lines" -eq 3 ]
+}
+
+#############################
+# get_root_partitions
+#############################
+
+@test "get_root_partitions: two SATA disks emit two suffix-2 partitions" {
+ run get_root_partitions /dev/sda /dev/sdb
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/sda2
+/dev/sdb2" ]
+}
+
+@test "get_root_partitions: mixed SATA + NVMe applies correct suffix per disk" {
+ run get_root_partitions /dev/sda /dev/nvme0n1
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/sda2
+/dev/nvme0n1p2" ]
+}
+
+@test "get_root_partitions: NVMe array gets p2 suffix on each entry" {
+ run get_root_partitions /dev/nvme0n1 /dev/nvme1n1
+ [ "$status" -eq 0 ]
+ [ "$output" = "/dev/nvme0n1p2
+/dev/nvme1n1p2" ]
+}