From 863ceeac8fdb10258a58d35bcee6874097fffc88 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 12 Apr 2026 23:48:14 -0400 Subject: test: add bats unit tests for common.sh and config.sh 23 bats tests covering the pure logic in installer/lib/common.sh (command_exists, require_command, info/warn/error, enable_color, require_root, log) and installer/lib/config.sh (parse_args, load_config, validate_config, check_config). Makefile adds a 'bats' target; 'test' now runs lint + bats (VM integration tests remain under test-install). --- tests/unit/test_common.bats | 88 +++++++++++++++++++++++++++++++++++ tests/unit/test_config.bats | 111 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 tests/unit/test_common.bats create mode 100644 tests/unit/test_config.bats (limited to 'tests') diff --git a/tests/unit/test_common.bats b/tests/unit/test_common.bats new file mode 100644 index 0000000..6b9c18f --- /dev/null +++ b/tests/unit/test_common.bats @@ -0,0 +1,88 @@ +#!/usr/bin/env bats +# Unit tests for installer/lib/common.sh + +setup() { + # shellcheck disable=SC1091 + source "${BATS_TEST_DIRNAME}/../../installer/lib/common.sh" +} + +@test "command_exists returns 0 for an existing command" { + run command_exists bash + [ "$status" -eq 0 ] +} + +@test "command_exists returns 1 for a missing command" { + run command_exists this_does_not_exist_xyz_42 + [ "$status" -eq 1 ] +} + +@test "require_command succeeds for an existing command" { + run require_command bash + [ "$status" -eq 0 ] +} + +@test "require_command fails and reports missing command" { + run require_command this_does_not_exist_xyz_42 + [ "$status" -eq 1 ] + [[ "$output" == *"Required command not found"* ]] + [[ "$output" == *"this_does_not_exist_xyz_42"* ]] +} + +@test "enable_color populates color variables" { + [ -z "$RED" ] + [ -z "$GREEN" ] + [ -z "$NC" ] + enable_color + [ -n "$RED" ] + [ -n "$GREEN" ] + [ -n "$YELLOW" ] + [ -n "$BLUE" ] + [ -n "$BOLD" ] + [ -n "$NC" ] +} + +@test "info prints [INFO] prefix and message" { + run info "hello world" + [ "$status" -eq 0 ] + [[ "$output" == *"[INFO]"* ]] + [[ "$output" == *"hello world"* ]] +} + +@test "warn prints [WARN] prefix and message" { + run warn "heads up" + [ "$status" -eq 0 ] + [[ "$output" == *"[WARN]"* ]] + [[ "$output" == *"heads up"* ]] +} + +@test "error prints [ERROR] and exits with status 1" { + run error "something broke" + [ "$status" -eq 1 ] + [[ "$output" == *"[ERROR]"* ]] + [[ "$output" == *"something broke"* ]] +} + +@test "require_root fails for non-root user" { + [ "$EUID" -ne 0 ] || skip "running as root" + run require_root + [ "$status" -eq 1 ] + [[ "$output" == *"must be run as root"* ]] +} + +@test "log writes timestamped line when LOG_FILE set" { + local tmp + tmp=$(mktemp) + LOG_FILE="$tmp" log "test entry" + run cat "$tmp" + [ "$status" -eq 0 ] + [[ "$output" == *"test entry"* ]] + [[ "$output" =~ \[[0-9]{4}-[0-9]{2}-[0-9]{2} ]] + rm -f "$tmp" +} + +@test "log is a no-op when LOG_FILE unset" { + unset LOG_FILE + run log "should not crash" + [ "$status" -eq 0 ] + [ -z "$output" ] +} diff --git a/tests/unit/test_config.bats b/tests/unit/test_config.bats new file mode 100644 index 0000000..c659fdc --- /dev/null +++ b/tests/unit/test_config.bats @@ -0,0 +1,111 @@ +#!/usr/bin/env bats +# Unit tests for installer/lib/config.sh + +setup() { + # shellcheck disable=SC1091 + source "${BATS_TEST_DIRNAME}/../../installer/lib/common.sh" + # shellcheck disable=SC1091 + source "${BATS_TEST_DIRNAME}/../../installer/lib/config.sh" +} + +@test "parse_args stores --config-file path" { + parse_args --config-file /tmp/foo.conf + [ "$CONFIG_FILE" = "/tmp/foo.conf" ] +} + +@test "parse_args rejects --config-file with no argument" { + run parse_args --config-file + [ "$status" -eq 1 ] + [[ "$output" == *"requires a path"* ]] +} + +@test "parse_args rejects --config-file when value looks like a flag" { + run parse_args --config-file --help + [ "$status" -eq 1 ] + [[ "$output" == *"requires a path"* ]] +} + +@test "parse_args --color enables color vars" { + [ -z "$RED" ] + parse_args --color + [ -n "$RED" ] +} + +@test "parse_args --help shows usage and exits 0" { + run parse_args --help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage:"* ]] + [[ "$output" == *"--config-file"* ]] +} + +@test "parse_args rejects unknown option" { + run parse_args --not-a-real-flag + [ "$status" -eq 1 ] + [[ "$output" == *"Unknown option"* ]] +} + +@test "load_config errors on missing file" { + run load_config /nonexistent/path/archangel.conf + [ "$status" -eq 1 ] + [[ "$output" == *"Config file not found"* ]] +} + +@test "load_config parses a minimal config and sets UNATTENDED" { + local tmp + tmp=$(mktemp) + cat >"$tmp" <<'EOF' +HOSTNAME=testhost +TIMEZONE=UTC +DISKS=/dev/sda,/dev/sdb +ROOT_PASSWORD=secret +EOF + load_config "$tmp" + [ "$HOSTNAME" = "testhost" ] + [ "$TIMEZONE" = "UTC" ] + [ "$ROOT_PASSWORD" = "secret" ] + [ "${SELECTED_DISKS[0]}" = "/dev/sda" ] + [ "${SELECTED_DISKS[1]}" = "/dev/sdb" ] + [ "$UNATTENDED" = "true" ] + rm -f "$tmp" +} + +@test "load_config parses a single-disk config into 1-element array" { + local tmp + tmp=$(mktemp) + echo "DISKS=/dev/nvme0n1" >"$tmp" + load_config "$tmp" + [ "${#SELECTED_DISKS[@]}" -eq 1 ] + [ "${SELECTED_DISKS[0]}" = "/dev/nvme0n1" ] + rm -f "$tmp" +} + +@test "validate_config fails and lists every missing required field" { + HOSTNAME="" + TIMEZONE="" + SELECTED_DISKS=() + ROOT_PASSWORD="" + run validate_config + [ "$status" -eq 1 ] + [[ "$output" == *"HOSTNAME not set"* ]] + [[ "$output" == *"TIMEZONE not set"* ]] + [[ "$output" == *"No disks selected"* ]] + [[ "$output" == *"ROOT_PASSWORD not set"* ]] + [[ "$output" == *"4 error"* ]] +} + +@test "validate_config rejects an invalid timezone" { + HOSTNAME="h" + TIMEZONE="Not/A_Real_Zone_xyz" + SELECTED_DISKS=() + ROOT_PASSWORD="x" + run validate_config + [ "$status" -eq 1 ] + [[ "$output" == *"Invalid timezone"* ]] +} + +@test "check_config is a no-op when CONFIG_FILE is unset" { + CONFIG_FILE="" + run check_config + [ "$status" -eq 0 ] + [ -z "$output" ] +} -- cgit v1.2.3