update to V2

This commit is contained in:
David Schroeder
2026-08-12 21:50:50 -05:00
parent 873bb17b62
commit 72d61e5785
41 changed files with 9421 additions and 477 deletions
Executable
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
declare -a shell_files=()
declare -a test_files=()
failures=0
required_command=''
if (( BASH_VERSINFO[0] < 4 ||
(BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3) )); then
printf 'TA-ProxMenu tests require Bash 4.3 or newer; found %s.\n' \
"$BASH_VERSION" >&2
exit 1
fi
for required_command in git python3; do
if ! command -v "$required_command" >/dev/null 2>&1; then
printf 'TA-ProxMenu tests require %s.\n' "$required_command" >&2
exit 1
fi
done
while IFS= read -r -d '' file; do
shell_files+=("$file")
done < <(
find "$TEST_ROOT" -type f \( -name '*.sh' -o -name '*.inc' \) \
-not -path '*/.git/*' -print0 |
sort -z
)
printf 'Bash syntax\n'
for file in "${shell_files[@]}"; do
if ! bash -n "$file"; then
(( failures += 1 ))
fi
done
(( failures == 0 )) && printf ' PASS: %d files\n' "${#shell_files[@]}"
printf '\nGit whitespace\n'
if git -C "$TEST_ROOT" diff --check &&
git -C "$TEST_ROOT" diff --cached --check; then
printf ' PASS\n'
else
(( failures += 1 ))
fi
printf '\nShellCheck\n'
if command -v shellcheck >/dev/null 2>&1; then
# These rules cannot follow TAPM's runtime-sourced defaults/colors or
# functions invoked indirectly through traps and menu dispatch.
if shellcheck -x -e SC1091,SC2034,SC2154,SC2317 "${shell_files[@]}"; then
printf ' PASS\n'
else
(( failures += 1 ))
fi
else
printf ' SKIP: shellcheck is not installed\n'
fi
while IFS= read -r -d '' file; do
test_files+=("$file")
done < <(
find "${TEST_ROOT}/tests" -maxdepth 1 -type f -name 'test-*.sh' \
-print0 | sort -z
)
printf '\nBehavior tests\n'
for file in "${test_files[@]}"; do
printf ' %-30s ' "${file##*/}"
if bash "$file"; then
:
else
(( failures += 1 ))
fi
done
printf '\n'
if (( failures > 0 )); then
printf 'FAILED: %d check group(s) failed.\n' "$failures" >&2
exit 1
fi
printf 'All TA-ProxMenu checks passed.\n'
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "${TEST_ROOT}/tests/testlib.sh"
source "${TEST_ROOT}/inc/cluster-update.inc"
nodes_json='[
{"node":"pve3","status":"offline"},
{"node":"pve1","status":"online"},
{"node":"pve2","status":"online"},
{"node":"bad node","status":"online"}
]'
expected=$'pve1\tonline\npve2\tonline\npve3\toffline'
assert_equal "$expected" \
"$(printf '%s' "$nodes_json" | TAPM_CLUSTER_NODES_FROM_JSON)" \
"cluster node discovery filters and sorts API output"
wrapped_json='{"data":[{"node":"pve2.example","status":"ONLINE"}]}'
assert_equal $'pve2.example\tonline' \
"$(printf '%s' "$wrapped_json" | TAPM_CLUSTER_NODES_FROM_JSON)" \
"wrapped API response and normalized status"
if printf '%s' 'not-json' | TAPM_CLUSTER_NODES_FROM_JSON 2>/dev/null; then
printf 'FAIL: malformed cluster JSON was accepted\n' >&2
exit 1
fi
assert_failure "remote node rejects shell characters" \
TAPM_UPDATE_REMOTE_NODE 'pve1;reboot'
test_dir="$(mktemp -d)"
trap 'rm -rf "$test_dir"' EXIT
TAPM_COROSYNC_CONFIG="${test_dir}/corosync.conf"
printf 'totem {}\n' >"$TAPM_COROSYNC_CONFIG"
update_log="${test_dir}/updates"
LightCyan=0
LightYellow=0
Green=0
Red=0
Default=0
idsCL[0]=''
pvesh() {
return 0
}
TAPM_CLUSTER_NODE_ROWS() {
printf 'pve1\tonline\npve2\tonline\npve3\toffline\n'
}
TAPM_LOCAL_CLUSTER_NODE() {
printf 'pve1\n'
}
TAPM_UPDATE_REMOTE_NODE() {
printf 'remote:%s\n' "$1" >>"$update_log"
}
INSTALL_LOCAL_UPDATES() {
printf 'local:pve1\n' >>"$update_log"
}
if INSTALL_CLUSTER_UPDATES >/dev/null; then
printf 'FAIL: cluster update ignored an offline node\n' >&2
exit 1
fi
assert_equal $'remote:pve2\nlocal:pve1' \
"$(cat "$update_log")" \
"online remotes and initiating node update once"
TAPM_CLUSTER_NODE_ROWS() {
printf 'pve1\tonline\npve2\tonline\n'
}
TAPM_UPDATE_REMOTE_NODE() {
printf 'remote update output\n'
}
INSTALL_LOCAL_UPDATES() {
printf 'local update output\n'
}
expected_output=$'Updating TA-ProxMenu across 2 cluster node(s)...\n\nUpdating remote node pve2...\nremote update output\nRemote node pve2 is updated.\n\nUpdating local node pve1...\nlocal update output\nLocal node pve1 is updated.\n\nTA-ProxMenu is updated on all 2 cluster node(s).'
assert_equal "$expected_output" \
"$(INSTALL_CLUSTER_UPDATES)" \
"each node update is grouped with blank lines only between node blocks"
finish_tests
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd "${TEST_DIR}/.." && pwd)"
source "${REPO_DIR}/inc/cpu-compat.inc"
assert_equal() {
local expected="${1:?expected value is required}"
local actual="${2:?actual value is required}"
local description="${3:?description is required}"
if [[ "$actual" != "$expected" ]]; then
printf 'FAIL: %s\nExpected: %s\nActual: %s\n' \
"$description" "$expected" "$actual" >&2
exit 1
fi
}
assert_equal \
"Broadwell-EP" \
"$(TAPM_CPU_GENERATION_FROM_NAME "Intel(R) Xeon(R) CPU E5-2690 v4 @ 2.60GHz")" \
"identifies Xeon E5 v4"
assert_equal \
"3rd Gen Xeon Scalable (Ice Lake)" \
"$(TAPM_CPU_GENERATION_FROM_NAME "Intel(R) Xeon(R) Gold 6338 CPU @ 2.00GHz")" \
"identifies third-generation Xeon Scalable"
assert_equal \
"3rd Gen EPYC (Milan / Zen 3)" \
"$(TAPM_CPU_GENERATION_FROM_NAME "AMD EPYC 7543 32-Core Processor")" \
"identifies EPYC Milan"
proxclmc_fixture='node-a | 10.0.0.1 | x86-64-v4
node-b | 10.0.0.2 | x86-64-v3
Cluster CPU type: x86-64-v3'
expected_rows=$'node-a\03410.0.0.1\034x86-64-v4\nnode-b\03410.0.0.2\034x86-64-v3'
actual_rows="$(printf '%s\n' "$proxclmc_fixture" | TAPM_PROXCLMC_HOST_ROWS)"
assert_equal "$expected_rows" "$actual_rows" "parses ProxCLMC host rows"
printf 'CPU compatibility tests passed.\n'
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "${TEST_ROOT}/tests/testlib.sh"
source "${TEST_ROOT}/inc/evacuate-proxmox-node.sh"
test_stopped_ha_guest_does_not_block_wait() {
LOCAL_NODE=node1
HA_WAIT_SECONDS=0
get_local_guests() {
printf '%s\n' \
$'100\x1fqemu\x1fstopped\x1fshutdown-ha-vm\x1f1024' \
$'101\x1fqemu\x1frunning\x1fnon-ha-vm\x1f1024'
}
get_ha_guest_ids() {
printf '%s\n' 100
}
wait_for_ha_evacuation
}
test_running_ha_guest_blocks_wait() {
LOCAL_NODE=node1
HA_WAIT_SECONDS=0
get_local_guests() {
printf '%s\n' $'100\x1fqemu\x1frunning\x1frunning-ha-vm\x1f1024'
}
get_ha_guest_ids() {
printf '%s\n' 100
}
wait_for_ha_evacuation
}
assert_success "stopped HA guest does not block evacuation wait" \
test_stopped_ha_guest_does_not_block_wait
assert_failure "running HA guest still blocks evacuation wait" \
test_running_ha_guest_blocks_wait
set_routing_fixture() {
MIGRATION_NODES=(node2 node3)
NODE_STORAGE_CACHE=()
NODE_STORAGE_CACHE[node2]='shared-a,shared-b'
NODE_STORAGE_CACHE[node3]='shared-a,shared-c'
PREFERRED_TARGET=node2
}
set_routing_fixture
assert_success "preferred destination is eligible" \
choose_destination shared-a '' 0
assert_equal node2 "$CHOSEN_DESTINATION" "preferred destination selected"
set_routing_fixture
assert_success "affinity fallback is available" \
choose_destination shared-a node3 1
assert_equal node3 "$CHOSEN_DESTINATION" "affinity destination selected"
assert_equal "routed to satisfy HA node-affinity preference" "$CHOSEN_NOTE" \
"affinity routing explanation"
set_routing_fixture
assert_success "storage fallback is available" \
choose_destination shared-c '' 0
assert_equal node3 "$CHOSEN_DESTINATION" "storage-compatible destination selected"
set_routing_fixture
assert_failure "strict affinity blocks two noncompliant destinations" \
choose_destination shared-a node4 1
set_routing_fixture
MIGRATION_NODES=(node2)
assert_success "sole eligible node overrides affinity" \
choose_destination shared-a node4 1
assert_equal node2 "$CHOSEN_DESTINATION" "sole eligible destination selected"
assert_equal "only eligible node; HA node-affinity preference overridden" \
"$CHOSEN_NOTE" "sole-node override explanation"
TEST_RULES_FILE="$(mktemp /tmp/tapm-ha-rules.XXXXXX)"
cleanup_evacuation_tests() {
if [[ "$TEST_RULES_FILE" == /tmp/tapm-ha-rules.* ]]; then
rm -f -- "$TEST_RULES_FILE"
fi
}
trap cleanup_evacuation_tests EXIT
printf '%s\n' \
'node-affinity: preferred-nodes' \
' resources vm:210,ct:211' \
' nodes node3:20,node2:10' \
' strict 1' >"$TEST_RULES_FILE"
HA_RULES_FILE="$TEST_RULES_FILE"
assert_equal $'preferred-nodes\x1f1\x1fnode3,node2' \
"$(get_guest_node_affinity qemu 210)" \
"QEMU affinity parsing"
assert_equal $'preferred-nodes\x1f1\x1fnode3,node2' \
"$(get_guest_node_affinity lxc 211)" \
"LXC affinity parsing"
assert_equal $'none\x1f0\x1f' \
"$(get_guest_node_affinity qemu 999)" \
"guest without affinity"
finish_tests
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
test_dir="$(mktemp -d)"
trap 'rm -rf "$test_dir"' EXIT
TAPM_FLEET_STATE_DIR="${test_dir}/state"
TAPM_FLEET_IDENTITY_FILE="${TAPM_FLEET_STATE_DIR}/identity.env"
TAPM_BROKER_URL='https://tapm.example.com'
source "${TEST_ROOT}/inc/fleet.inc"
TAPM_FLEET_ENSURE_IDENTITY
if ! TAPM_FLEET_VALID_IDENTITY; then
printf 'FAIL: generated fleet identity is invalid\n' >&2
exit 1
fi
if stat -c '%a' "$TAPM_FLEET_IDENTITY_FILE" >/dev/null 2>&1; then
identity_mode="$(stat -c '%a' "$TAPM_FLEET_IDENTITY_FILE")"
else
identity_mode="$(stat -f '%Lp' "$TAPM_FLEET_IDENTITY_FILE")"
fi
if [[ "$identity_mode" != 600 ]]; then
printf 'FAIL: fleet identity mode is not 600\n' >&2
exit 1
fi
TAPM_FLEET_VERSION='2026.7.26-7'
TAPM_FLEET_HOSTNAME='pve01'
TAPM_FLEET_GIT_COMMIT='0123456789abcdef0123456789abcdef01234567'
TAPM_FLEET_PVE_VERSION='pve-manager/9.0.3/abc~1'
TAPM_FLEET_OS_VERSION='Debian GNU/Linux 13 (trixie)'
TAPM_FLEET_KERNEL_VERSION='6.14.11-2-pve'
TAPM_FLEET_ARCHITECTURE='amd64'
TAPM_FLEET_CLUSTERED=true
registration_json="$(
TAPM_FLEET_INCLUDE_CREDENTIAL=1 TAPM_FLEET_JSON installed success
)"
REGISTRATION_JSON="$registration_json" python3 -c '
import json, os
payload = json.loads(os.environ["REGISTRATION_JSON"])
assert payload["installation_id"]
assert payload["hostname"] == "pve01"
assert len(payload["credential"]) == 64
assert payload["proxmenu_version"] == "2026.7.26-7"
assert payload["clustered"] is True
'
printf '0\n' >"${test_dir}/curl-count"
curl_should_fail=0
curl() {
local count data_path='' previous='' argument=''
count="$(cat "${test_dir}/curl-count")"
count="$((count + 1))"
printf '%s\n' "$count" >"${test_dir}/curl-count"
for argument in "$@"; do
if [[ "$previous" == '--data-binary' ]]; then
data_path="${argument#@}"
fi
previous="$argument"
done
if [[ -n "$data_path" && "$data_path" != '-' ]]; then
cp "$data_path" "${test_dir}/curl-body-${count}.json"
fi
cat >"${test_dir}/curl-config-${count}"
(( curl_should_fail == 0 ))
}
TAPM_FLEET_REGISTERED=1
TAPM_FLEET_EVENT_SEND run_completed success '' 12
EVENT_BODY="$(cat "${test_dir}/curl-body-1.json")" \
EVENT_CONFIG="$(cat "${test_dir}/curl-config-1")" \
EXPECTED_CREDENTIAL="$TAPM_FLEET_CREDENTIAL" \
python3 -c '
import json, os
payload = json.loads(os.environ["EVENT_BODY"])
assert payload["event"] == "run_completed"
assert payload["hostname"] == "pve01"
assert payload["result"] == "success"
assert payload["duration_seconds"] == 12
assert "credential" not in payload
config = os.environ["EVENT_CONFIG"]
assert "Authorization: Bearer " + os.environ["EXPECTED_CREDENTIAL"] in config
assert "https://tapm.example.com/api/v1/hosts/events" in config
'
if compgen -G "${TMPDIR:-/tmp}/tapm-fleet-event.*" >/dev/null; then
printf 'FAIL: fleet event left a temporary payload file\n' >&2
exit 1
fi
printf '0\n' >"${test_dir}/curl-count"
TAPM_FLEET_ENROLLED=0
TAPM_FLEET_REGISTERED=0
TAPM_FLEET_LAST_VERSION=''
TAPM_FLEET_COLLECT_METADATA() {
TAPM_FLEET_HOSTNAME='pve01'
TAPM_FLEET_GIT_COMMIT='0123456789abcdef0123456789abcdef01234567'
TAPM_FLEET_PVE_VERSION='pve-manager/9.2.4'
TAPM_FLEET_OS_VERSION='Debian GNU/Linux 13 (trixie)'
TAPM_FLEET_KERNEL_VERSION='6.14.11-5-pve'
TAPM_FLEET_ARCHITECTURE='amd64'
TAPM_FLEET_CLUSTERED=true
}
TAPM_FLEET_START '2026.7.28-9'
TAPM_FLEET_FINISH 0
if [[ "$(cat "${test_dir}/curl-count")" != 2 ]]; then
printf 'FAIL: initial run did not make exactly registration + completion calls\n' >&2
exit 1
fi
TAPM_FLEET_LOAD_IDENTITY
if [[ "$TAPM_FLEET_ENROLLED" != 1 ||
"$TAPM_FLEET_LAST_VERSION" != '2026.7.28-9' ]]; then
printf 'FAIL: successful fleet enrollment was not persisted\n' >&2
exit 1
fi
TAPM_FLEET_REGISTERED=0
TAPM_FLEET_START '2026.7.28-9'
TAPM_FLEET_FINISH 0
if [[ "$(cat "${test_dir}/curl-count")" != 3 ]]; then
printf 'FAIL: normal run made more than one broker call\n' >&2
exit 1
fi
curl_should_fail=1
TAPM_FLEET_REGISTERED=0
TAPM_FLEET_START '2026.7.28-9'
TAPM_FLEET_FINISH 0
TAPM_FLEET_LOAD_IDENTITY
if [[ "$TAPM_FLEET_ENROLLED" != 0 ]]; then
printf 'FAIL: failed fleet event did not schedule next-run re-enrollment\n' >&2
exit 1
fi
curl_should_fail=0
TAPM_FLEET_REGISTERED=0
TAPM_FLEET_START '2026.7.28-9'
TAPM_FLEET_FINISH 0
TAPM_FLEET_LOAD_IDENTITY
if [[ "$TAPM_FLEET_ENROLLED" != 1 ]]; then
printf 'FAIL: fleet client did not recover enrollment after a failed event\n' >&2
exit 1
fi
printf 'PASS: fleet identity and event client\n'
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "${TEST_ROOT}/tests/testlib.sh"
source "${TEST_ROOT}/inc/git-update.inc"
assert_success "automatic Git check cache is 24 hours" \
grep -q '^UPDATE_CACHE_SECONDS=86400$' "${TEST_ROOT}/proxmenu-scripts.sh"
TEST_TEMP_DIR="$(mktemp -d /tmp/tapm-git-tests.XXXXXX)"
TEST_REPOSITORY="${TEST_TEMP_DIR}/repository"
cleanup_git_tests() {
if [[ "$TEST_TEMP_DIR" == /tmp/tapm-git-tests.* && -d "$TEST_TEMP_DIR" ]]; then
rm -rf -- "$TEST_TEMP_DIR"
fi
}
trap cleanup_git_tests EXIT
git init -q -b main "$TEST_REPOSITORY"
git -C "$TEST_REPOSITORY" config user.name 'TA-ProxMenu Tests'
git -C "$TEST_REPOSITORY" config user.email 'tapm-tests@example.invalid'
printf 'first\n' >"${TEST_REPOSITORY}/fixture.txt"
git -C "$TEST_REPOSITORY" add fixture.txt
git -C "$TEST_REPOSITORY" commit -q -m first
commit_a="$(git -C "$TEST_REPOSITORY" rev-parse HEAD)"
printf 'second\n' >>"${TEST_REPOSITORY}/fixture.txt"
git -C "$TEST_REPOSITORY" commit -q -am second
commit_b="$(git -C "$TEST_REPOSITORY" rev-parse HEAD)"
git -C "$TEST_REPOSITORY" switch -q --detach "$commit_a"
printf 'alternate\n' >"${TEST_REPOSITORY}/alternate.txt"
git -C "$TEST_REPOSITORY" add alternate.txt
git -C "$TEST_REPOSITORY" commit -q -m alternate
commit_c="$(git -C "$TEST_REPOSITORY" rev-parse HEAD)"
git -C "$TEST_REPOSITORY" update-ref refs/tapm/a "$commit_a"
git -C "$TEST_REPOSITORY" update-ref refs/tapm/b "$commit_b"
git -C "$TEST_REPOSITORY" update-ref refs/tapm/c "$commit_c"
assert_equal current \
"$(TAPM_GIT_RELATION "$TEST_REPOSITORY" refs/tapm/a refs/tapm/a)" \
"equal commits"
assert_equal behind \
"$(TAPM_GIT_RELATION "$TEST_REPOSITORY" refs/tapm/a refs/tapm/b)" \
"local commit behind remote"
assert_equal ahead \
"$(TAPM_GIT_RELATION "$TEST_REPOSITORY" refs/tapm/b refs/tapm/a)" \
"local commit ahead of remote"
assert_equal diverged \
"$(TAPM_GIT_RELATION "$TEST_REPOSITORY" refs/tapm/b refs/tapm/c)" \
"diverged commits"
git -C "$TEST_REPOSITORY" switch -q -C main "$commit_a"
git -C "$TEST_REPOSITORY" update-ref refs/remotes/origin/main "$commit_a"
assert_equal current \
"$(TAPM_GIT_BRANCH_STATE "$TEST_REPOSITORY" main)" \
"clean current branch"
printf 'untracked\n' >"${TEST_REPOSITORY}/untracked.txt"
assert_equal dirty \
"$(TAPM_GIT_BRANCH_STATE "$TEST_REPOSITORY" main)" \
"dirty working tree"
rm -f -- "${TEST_REPOSITORY}/untracked.txt"
git -C "$TEST_REPOSITORY" switch -q -c feature
assert_equal wrong-branch \
"$(TAPM_GIT_BRANCH_STATE "$TEST_REPOSITORY" main)" \
"wrong checked-out branch"
git -C "$TEST_REPOSITORY" switch -q main
git -C "$TEST_REPOSITORY" update-ref refs/remotes/origin/main "$commit_b"
assert_success "fast-forward merge" \
TAPM_GIT_FAST_FORWARD "$TEST_REPOSITORY" main
assert_equal "$commit_b" \
"$(git -C "$TEST_REPOSITORY" rev-parse HEAD)" \
"fast-forward destination"
finish_tests
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "${TEST_ROOT}/tests/testlib.sh"
source "${TEST_ROOT}/inc/ha-status.inc"
STATUS_FILE="$(mktemp /tmp/tapm-ha-status.XXXXXX)"
cleanup_ha_status_test() {
[[ "$STATUS_FILE" == /tmp/tapm-ha-status.* ]] && rm -f -- "$STATUS_FILE"
}
trap cleanup_ha_status_test EXIT
printf '%s\n' \
'{"node_status":{"pve1":"online","pve2":"maintenance mode"}}' \
>"$STATUS_FILE"
assert_success "maintenance node detected in HA manager state" \
TAPM_HA_NODE_IN_MAINTENANCE pve2 "$STATUS_FILE"
assert_failure "active node is not reported as maintenance" \
TAPM_HA_NODE_IN_MAINTENANCE pve1 "$STATUS_FILE"
printf '%s\n' \
'{"nodes":[{"node":"pve3","state":"maintenance"}]}' \
>"$STATUS_FILE"
assert_success "structured HA node state detected" \
TAPM_HA_NODE_IN_MAINTENANCE pve3 "$STATUS_FILE"
printf '%s\n' 'not-json' >"$STATUS_FILE"
assert_failure "malformed HA state is rejected" \
TAPM_HA_NODE_IN_MAINTENANCE pve1 "$STATUS_FILE"
finish_tests
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
test_dir="$(mktemp -d)"
trap 'rm -rf "$test_dir"' EXIT
source "${TEST_ROOT}/tests/testlib.sh"
source "${TEST_ROOT}/inc/header-info.inc"
pveversion() {
printf '%s\n' 'pve-manager/9.2.4/5e5ae681198514d4 (running kernel: 7.0.14-5-pve)'
}
cat >"${test_dir}/corosync.conf" <<'EOF'
totem {
version: 2
cluster_name: production-cluster
}
EOF
TAPM_LOAD_HEADER_INFO "${test_dir}/corosync.conf"
assert_equal '9.2.4' "$TAPM_HEADER_PVE_VERSION" \
"header extracts the concise Proxmox VE version"
assert_equal 'production-cluster' "$TAPM_HEADER_CLUSTER" \
"header reads the local cluster name"
pveversion() {
printf '%s\n' 'pve-manager/10.0.0/changed'
}
TAPM_LOAD_HEADER_INFO "${test_dir}/corosync.conf"
assert_equal '9.2.4' "$TAPM_HEADER_PVE_VERSION" \
"header information is cached between menu redraws"
TAPM_HEADER_INFO_LOADED=0
TAPM_HEADER_PVE_VERSION='Unavailable'
TAPM_HEADER_CLUSTER='Standalone'
TAPM_LOAD_HEADER_INFO "${test_dir}/missing.conf"
assert_equal '10.0.0' "$TAPM_HEADER_PVE_VERSION" \
"header refresh reads the current Proxmox VE version"
assert_equal 'Standalone' "$TAPM_HEADER_CLUSTER" \
"host without corosync configuration is shown as standalone"
finish_tests
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "${TEST_ROOT}/tests/testlib.sh"
source "${TEST_ROOT}/inc/deploy-iso-nfs-lxc.sh"
assert_success "LXC creation uses the compatible cpuunits option" \
grep -q -- '--cpuunits 100' "${TEST_ROOT}/inc/deploy-iso-nfs-lxc.sh"
assert_failure "misspelled cpunits option is absent" \
grep -q -- '--cpunits' "${TEST_ROOT}/inc/deploy-iso-nfs-lxc.sh"
assert_success "valid storage ID" TAPM_ISO_NFS_VALID_ID PVE-Shared-Storage
assert_failure "storage ID cannot start with a number" TAPM_ISO_NFS_VALID_ID 1-storage
assert_failure "storage ID rejects spaces" TAPM_ISO_NFS_VALID_ID 'shared storage'
assert_success "valid CTID" TAPM_ISO_NFS_VALID_CTID 210
assert_failure "reserved two-digit CTID" TAPM_ISO_NFS_VALID_CTID 99
assert_failure "CTID rejects text" TAPM_ISO_NFS_VALID_CTID ct210
assert_success "valid hostname" TAPM_ISO_NFS_VALID_HOSTNAME PVE-Shared-Storage
assert_success "valid FQDN hostname" TAPM_ISO_NFS_VALID_HOSTNAME iso-nfs.example.net
assert_failure "hostname rejects spaces" TAPM_ISO_NFS_VALID_HOSTNAME 'ISO Storage'
assert_success "valid IPv4 CIDR" TAPM_ISO_NFS_VALID_IPV4_CIDR 10.10.2.45/16
assert_failure "IPv4 octet out of range" TAPM_ISO_NFS_VALID_IPV4_CIDR 10.10.2.256/24
assert_failure "IPv4 prefix out of range" TAPM_ISO_NFS_VALID_IPV4_CIDR 10.10.2.45/33
assert_failure "IPv4 prefix required" TAPM_ISO_NFS_VALID_IPV4_CIDR 10.10.2.45
pvesm() {
[[ "${1:-}" == status ]] || return 1
printf '%s\n' \
'Name Type Status Total Used Available %' \
'local-lvm lvmthin active 100 10 90 10%' \
'iSCSI-Datastore1 lvm active 200 20 180 10%' \
'iSCSI-Datastore1-2 lvm active 200 20 180 10%' \
'offline-store dir inactive 100 0 100 0%'
}
SELECT_MENU() {
local title="$1"
assert_equal "Root filesystem storage" "$title" "storage menu title"
assert_equal \
'iSCSI-Datastore1-2 (lvm) — default' \
"${labels[0]}" \
"default storage placed first"
assert_equal 3 "${#labels[@]}" "only active storages offered"
MENU_SELECTION="${values[0]}"
}
test_default_storage_selection() {
local selected_storage=''
TAPM_ISO_NFS_SELECT_STORAGE selected_storage \
"Root filesystem storage" "iSCSI-Datastore1-2" || return 1
assert_equal iSCSI-Datastore1-2 "$selected_storage" \
"pressing Enter retains default storage"
}
assert_success "default storage menu selection" test_default_storage_selection
pveam() {
[[ "${1:-}" == list && "${2:-}" == local ]] || return 1
printf '%s\n' \
'NAME VOLID FORMAT TYPE SIZE VMID' \
'local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst 0 0 0 0 0' \
'local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst 0 0 0 0 0'
}
assert_equal \
'local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst' \
"$(TAPM_ISO_NFS_LOCAL_TEMPLATE local)" \
"newest local Debian template avoids catalog refresh"
finish_tests
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "${TEST_ROOT}/tests/testlib.sh"
source "${TEST_ROOT}/inc/post-install.inc"
assert_equal 9 \
"$(printf '%s\n' 'pve-manager/9.0.3/0255bb4e9600f70c (running kernel: 6.14.8-2-pve)' |
sed -n 's/.*pve-manager\/\([0-9][0-9]*\).*/\1/p')" \
"Proxmox major version extraction"
POST_TEST_ROOT="$(mktemp -d /tmp/tapm-post-install.XXXXXX)"
TAPM_HOST_ROOT="$POST_TEST_ROOT"
cleanup_post_install_tests() {
if [[ "$POST_TEST_ROOT" == /tmp/tapm-post-install.* &&
-d "$POST_TEST_ROOT" ]]; then
rm -rf -- "$POST_TEST_ROOT"
fi
}
trap cleanup_post_install_tests EXIT
mkdir -p \
"${POST_TEST_ROOT}/etc/apt/apt.conf.d" \
"${POST_TEST_ROOT}/etc/systemd" \
"${POST_TEST_ROOT}/etc/logrotate.d" \
"${POST_TEST_ROOT}/etc/sysctl.d" \
"${POST_TEST_ROOT}/etc/network" \
"${POST_TEST_ROOT}/etc" \
"${POST_TEST_ROOT}/bin"
printf 'Acquire::Languages "none";\n' \
>"${POST_TEST_ROOT}/etc/apt/apt.conf.d/99-disable-translations"
assert_success "ProxMenux APT language setting recognized" \
TAPM_POST_EXACT_APT_LANGUAGES
printf 'Acquire::Languages "en";\n' \
>"${POST_TEST_ROOT}/etc/apt/apt.conf.d/99-disable-translations"
assert_failure "custom APT language setting preserved" \
TAPM_POST_EXACT_APT_LANGUAGES
cat >"${POST_TEST_ROOT}/etc/systemd/journald.conf" <<'EOF'
[Journal]
Storage=persistent
Seal=no
SystemMaxUse=64M
RuntimeMaxUse=60M
MaxLevelStore=info
EOF
assert_success "ProxMenux journald replacement recognized" \
TAPM_POST_PROXMENUX_JOURNALD
cat >"${POST_TEST_ROOT}/etc/logrotate.conf" <<'EOF'
# ProxMenux optimized configuration (Log2RAM-friendly)
daily
rotate 7
size 10M
copytruncate
include /etc/logrotate.d
EOF
assert_success "ProxMenux logrotate replacement recognized" \
TAPM_POST_PROXMENUX_LOGROTATE
cat >"${POST_TEST_ROOT}/bin/gzip" <<'EOF'
#!/bin/sh
GZIP="-1"
exec /usr/bin/pigz "$@"
EOF
cat >"${POST_TEST_ROOT}/bin/gzip.original" <<'EOF'
#!/bin/sh
if [ "${1:-}" = "--version" ]; then
printf 'gzip 1.13\n'
else
cat
fi
EOF
cp "${POST_TEST_ROOT}/bin/gzip" "${POST_TEST_ROOT}/bin/pigzwrapper"
chmod 0755 \
"${POST_TEST_ROOT}/bin/gzip" \
"${POST_TEST_ROOT}/bin/gzip.original" \
"${POST_TEST_ROOT}/bin/pigzwrapper"
assert_success "ProxMenux gzip wrapper recognized" \
TAPM_POST_PROXMENUX_GZIP_WRAPPER
assert_success "ProxMenux gzip wrapper repaired from valid original" \
TAPM_POST_REPAIR_GZIP
assert_failure "repaired gzip is no longer recognized as wrapper" \
TAPM_POST_PROXMENUX_GZIP_WRAPPER
assert_failure "obsolete gzip original removed after verification" \
test -e "${POST_TEST_ROOT}/bin/gzip.original"
assert_success "kernel panic profile written" TAPM_POST_CONFIGURE_PANIC
assert_success "inotify profile written" TAPM_POST_CONFIGURE_LIMITS
assert_success "memory profile written" TAPM_POST_CONFIGURE_MEMORY
assert_success "network profile written" TAPM_POST_CONFIGURE_NETWORK
assert_success "BBR profile written" TAPM_POST_CONFIGURE_BBR
assert_equal 30 \
"$(awk '/kernel.panic =/ {print $3}' \
"${POST_TEST_ROOT}/etc/sysctl.d/99-ta-proxmenu-kernel-panic.conf")" \
"kernel panic delay"
assert_equal 524288 \
"$(awk '/max_user_watches/ {print $3}' \
"${POST_TEST_ROOT}/etc/sysctl.d/99-ta-proxmenu-limits.conf")" \
"inotify watch limit"
assert_equal 10 \
"$(awk '/vm.swappiness/ {print $3}' \
"${POST_TEST_ROOT}/etc/sysctl.d/99-ta-proxmenu-memory.conf")" \
"host swappiness"
cat >"${POST_TEST_ROOT}/etc/vzdump.conf" <<'EOF'
# existing setting
bwlimit: 50000
pigz: 4
pigz: 8
EOF
assert_success "vzdump key replacement" \
TAPM_POST_SET_COLON_KEY /etc/vzdump.conf pigz 1
assert_equal 1 \
"$(awk -F: '/^pigz:/ {gsub(/[[:space:]]/, "", $2); print $2}' \
"${POST_TEST_ROOT}/etc/vzdump.conf")" \
"vzdump pigz setting is unique"
assert_success "vzdump key removal" \
TAPM_POST_REMOVE_COLON_KEY /etc/vzdump.conf bwlimit
assert_failure "vzdump bandwidth setting removed" \
grep -q '^bwlimit:' "${POST_TEST_ROOT}/etc/vzdump.conf"
printf 'deb should-be-backed-up\n' \
>"${POST_TEST_ROOT}/etc/apt/sources.list"
TAPM_POST_BACKUP_BASE='/var/backups/ta-proxmenu/post-install'
test_post_backup() {
TAPM_POST_BACKUP >/dev/null
}
assert_success "host configuration backup created" test_post_backup
printf 'changed\n' >"${POST_TEST_ROOT}/etc/apt/sources.list"
assert_success "host configuration backup restored" \
TAPM_POST_RESTORE_BACKUP "$TAPM_POST_LAST_BACKUP"
assert_equal 'deb should-be-backed-up' \
"$(cat "${POST_TEST_ROOT}/etc/apt/sources.list")" \
"restored APT sources"
assert_success "APT source layout enforced in fixture root" \
TAPM_POST_ENSURE_APT_LAYOUT
assert_equal 0 \
"$(wc -c <"${POST_TEST_ROOT}/etc/apt/sources.list" | tr -d ' ')" \
"main APT sources list is empty"
mkdir -p \
"${POST_TEST_ROOT}/etc/security/limits.d" \
"${POST_TEST_ROOT}/usr/local/share/proxmenux" \
"${POST_TEST_ROOT}/usr/local/bin"
cat >"${POST_TEST_ROOT}/etc/sysctl.d/99-network.conf" <<'EOF'
# Custom administrator network profile
net.ipv4.ip_forward = 1
EOF
cat >"${POST_TEST_ROOT}/etc/security/limits.d/99-limits.conf" <<'EOF'
# ProxMenux configuration
* soft nofile 1048576
EOF
cat >"${POST_TEST_ROOT}/usr/local/bin/menu" <<'EOF'
#!/bin/sh
exec /usr/local/share/proxmenux/menu.sh
EOF
assert_success "recognized ProxMenux settings cleaned" \
TAPM_POST_CLEAN_PROXMENUX_SETTINGS
assert_success "custom similarly named network profile retained" \
test -f "${POST_TEST_ROOT}/etc/sysctl.d/99-network.conf"
assert_failure "recognized ProxMenux limits profile removed" \
test -e "${POST_TEST_ROOT}/etc/security/limits.d/99-limits.conf"
assert_success "ProxMenux application removed" TAPM_POST_REMOVE_PROXMENUX_APP
assert_failure "ProxMenux menu launcher removed" \
test -e "${POST_TEST_ROOT}/usr/local/bin/menu"
finish_tests
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "${TEST_ROOT}/tests/testlib.sh"
TAPM_PULSE_STANDALONE_NO_MAIN=1
source "${TEST_ROOT}/install-pulse.sh"
assert_success "default V2 source branch accepted" \
TAPM_PULSE_VALID_SOURCE_BRANCH V2
assert_success "nested release branch accepted" \
TAPM_PULSE_VALID_SOURCE_BRANCH releases/pulse-6
assert_failure "empty source branch rejected" \
TAPM_PULSE_VALID_SOURCE_BRANCH ''
assert_failure "source branch traversal rejected" \
TAPM_PULSE_VALID_SOURCE_BRANCH '../main'
assert_success "HTTPS source base accepted" \
TAPM_PULSE_VALID_SOURCE_BASE \
https://tagit.technologyarch.com/TAI/TA-ProxMenu/raw/branch/V2
assert_failure "HTTP source base rejected" \
TAPM_PULSE_VALID_SOURCE_BASE \
http://tagit.technologyarch.com/TAI/TA-ProxMenu/raw/branch/V2
finish_tests
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "${TEST_ROOT}/tests/testlib.sh"
source "${TEST_ROOT}/inc/deploy-pulse-lxc.sh"
assert_success "stable Pulse release" TAPM_PULSE_VALID_RELEASE v6.1.1
assert_success "prerelease Pulse release" TAPM_PULSE_VALID_RELEASE v6.2.0-rc.1
assert_failure "Pulse release requires v prefix" TAPM_PULSE_VALID_RELEASE 6.1.1
assert_failure "Pulse release rejects URL content" \
TAPM_PULSE_VALID_RELEASE 'v6.1.1/../../latest'
assert_equal amd64 "$(TAPM_PULSE_ARCH x86_64)" "x86 architecture mapping"
assert_equal arm64 "$(TAPM_PULSE_ARCH aarch64)" "ARM architecture mapping"
assert_failure "unsupported Pulse architecture" TAPM_PULSE_ARCH riscv64
assert_equal 2 "$(TAPM_PULSE_BACKOFF_NEXT 1 8)" "Pulse backoff doubles"
assert_equal 8 "$(TAPM_PULSE_BACKOFF_NEXT 4 8)" "Pulse backoff reaches cap"
assert_equal 8 "$(TAPM_PULSE_BACKOFF_NEXT 8 8)" "Pulse backoff remains capped"
test_bridge_selection_assignment() {
local bridge=''
TAPM_PULSE_SET_BRIDGE_FROM_SELECTION bridge 'bridge:vmbr0' || return 1
assert_equal vmbr0 "$bridge" "selected Pulse bridge assigned to caller"
}
assert_success "Pulse bridge selection assignment" test_bridge_selection_assignment
assert_failure "empty Pulse bridge selection rejected" \
TAPM_PULSE_SET_BRIDGE_FROM_SELECTION selected_bridge 'bridge:'
bootstrap_token='0123456789abcdef0123456789abcdef0123456789abcdef'
bootstrap_output="║ Token: ${bootstrap_token} ║"
assert_equal "$bootstrap_token" \
"$(TAPM_PULSE_BOOTSTRAP_TOKEN_FROM_OUTPUT "$bootstrap_output")" \
"Pulse bootstrap token parsed"
assert_failure "malformed Pulse bootstrap output rejected" \
TAPM_PULSE_BOOTSTRAP_TOKEN_FROM_OUTPUT 'Token: not-a-token'
agent_token='0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
assert_equal "$agent_token" \
"$(TAPM_PULSE_TOKEN_FROM_RESPONSE "{\"token\":\"${agent_token}\"}")" \
"Pulse agent token parsed"
assert_failure "missing Pulse agent token rejected" \
TAPM_PULSE_TOKEN_FROM_RESPONSE '{"command":"install"}'
assert_failure "Pulse agent token containing whitespace rejected" \
TAPM_PULSE_TOKEN_FROM_RESPONSE '{"token":"invalid token"}'
assert_failure "non-hex Pulse agent token rejected" \
TAPM_PULSE_TOKEN_FROM_RESPONSE '{"token":"not-a-token"}'
assert_success "registered Pulse agent response accepted" \
TAPM_PULSE_AGENT_REGISTERED_FROM_RESPONSE \
'{"agent":{"id":"agent-123","hostname":"pve1"}}'
assert_failure "missing Pulse agent ID rejected" \
TAPM_PULSE_AGENT_REGISTERED_FROM_RESPONSE '{"agent":{"hostname":"pve1"}}'
assert_equal '{"type":"pve","enableCommands":true}' \
"$(TAPM_PULSE_AGENT_TOKEN_REQUEST_JSON)" \
"Pulse PVE unified-agent enrollment requested with commands enabled"
setup_token='0123456789abcdef0123456789abcdef'
setup_host='https://pve1.example.test:8006'
setup_old_url='https://pulse.example.test/api/setup-script?backup_perms=true'
setup_response="$(
SETUP_TOKEN="$setup_token" SETUP_HOST="$setup_host" SETUP_OLD_URL="$setup_old_url" \
python3 -c '
import json, os, sys, time
old_url = os.environ["SETUP_OLD_URL"]
token = os.environ["SETUP_TOKEN"]
json.dump({
"type": "pve",
"host": os.environ["SETUP_HOST"],
"url": old_url,
"downloadURL": old_url + "&setup_token=" + token,
"scriptFileName": "pulse-setup-pve.sh",
"command": old_url + " PULSE_SETUP_TOKEN=" + token + " if",
"commandWithEnv": old_url + " PULSE_SETUP_TOKEN=" + token + " if",
"commandWithoutEnv": old_url + " if",
"expires": int(time.time()) + 300,
"setupToken": token,
"tokenHint": token[:3] + "..." + token[-3:],
}, sys.stdout)
'
)"
normalized_setup="$(
TAPM_PULSE_NORMALIZE_V611_SETUP_ARTIFACT \
"$setup_response" 'http://10.10.2.30:7655'
)"
assert_equal "$setup_token" \
"$(SETUP_RESPONSE="$normalized_setup" python3 -c 'import json, os; print(json.loads(os.environ["SETUP_RESPONSE"])["setupToken"])')" \
"Pulse v6.1.1 compatibility preserves server-issued setup token"
assert_equal \
'http://10.10.2.30:7655/api/setup-script?host=https%3A%2F%2Fpve1.example.test%3A8006&pulse_url=http%3A%2F%2F10.10.2.30%3A7655&type=pve' \
"$(SETUP_RESPONSE="$normalized_setup" python3 -c 'import json, os; print(json.loads(os.environ["SETUP_RESPONSE"])["url"])')" \
"Pulse v6.1.1 compatibility normalizes setup artifact URL"
assert_failure "Pulse v6.1.1 compatibility rejects missing setup token" \
TAPM_PULSE_NORMALIZE_V611_SETUP_ARTIFACT \
'{"type":"pve","host":"https://pve1:8006"}' 'http://10.10.2.30:7655'
test_pve_token_output_assignment() {
local token_output=''
local token_status=99
pveum() {
printf '%s\n' \
'{"full-tokenid":"pulse-monitor@pve!pulse-test","value":"secret-value"}'
}
TAPM_PULSE_CREATE_PVE_AUTO_REGISTER_TOKEN \
'pulse-test' token_output token_status || return 1
unset -f pveum
assert_equal \
'{"full-tokenid":"pulse-monitor@pve!pulse-test","value":"secret-value"}' \
"$token_output" \
"Pulse receives pveum token output"
assert_equal 0 "$token_status" "Pulse receives pveum token status"
}
assert_success "Pulse v6.1.1 pveum output-shadowing workaround" \
test_pve_token_output_assignment
nodes_json='[
{"node":"pve3","status":"offline"},
{"node":"pve2","status":"online"},
{"node":"pve1","status":"online"}
]'
assert_equal $'pve1\npve2' \
"$(TAPM_PULSE_ONLINE_NODES_FROM_JSON "$nodes_json")" \
"online Pulse nodes selected and sorted"
assert_equal 'pve3' \
"$(TAPM_PULSE_OFFLINE_NODES_FROM_JSON "$nodes_json")" \
"offline Pulse nodes reported"
assert_success "valid Pulse CTID" TAPM_PULSE_VALID_CTID 210
assert_failure "invalid Pulse CTID" TAPM_PULSE_VALID_CTID 99
ctid_resources='[
{"type":"qemu","vmid":200},
{"type":"lxc","vmid":"201"},
{"type":"storage","storage":"local"}
]'
assert_equal 202 \
"$(TAPM_PULSE_FIRST_AVAILABLE_CTID_FROM_RESOURCES "$ctid_resources" 200)" \
"first available Pulse CTID starts at 200 and skips occupied IDs"
assert_equal 200 \
"$(TAPM_PULSE_FIRST_AVAILABLE_CTID_FROM_RESOURCES '[]' 200)" \
"Pulse CTID 200 selected when available"
assert_failure "invalid Pulse CTID resource data rejected" \
TAPM_PULSE_FIRST_AVAILABLE_CTID_FROM_RESOURCES '{}' 200
assert_success "valid Pulse hostname" TAPM_PULSE_VALID_HOSTNAME pulse-monitor
assert_failure "invalid Pulse hostname" TAPM_PULSE_VALID_HOSTNAME 'pulse monitor'
assert_success "valid positive integer" TAPM_PULSE_VALID_POSITIVE_INTEGER 1024
assert_failure "zero is not positive" TAPM_PULSE_VALID_POSITIVE_INTEGER 0
assert_success "zero is a valid nonnegative integer" \
TAPM_PULSE_VALID_NONNEGATIVE_INTEGER 0
assert_failure "negative integer rejected" TAPM_PULSE_VALID_NONNEGATIVE_INTEGER -1
assert_success "valid Pulse port" TAPM_PULSE_VALID_PORT 7655
assert_failure "Pulse port too high" TAPM_PULSE_VALID_PORT 65536
assert_success "valid Pulse IPv4 CIDR" TAPM_PULSE_VALID_IPV4_CIDR 10.10.1.50/24
assert_failure "invalid Pulse IPv4 CIDR" TAPM_PULSE_VALID_IPV4_CIDR 10.10.1.500/24
assert_success "valid Pulse gateway" TAPM_PULSE_VALID_IPV4 10.10.0.1
assert_failure "gateway CIDR rejected" TAPM_PULSE_VALID_IPV4 10.10.0.1/16
assert_success "blank optional Pulse address accepted" \
TAPM_PULSE_VALID_OPTIONAL_IPV4_CIDR ''
assert_success "valid Pulse VLAN" TAPM_PULSE_VALID_OPTIONAL_VLAN 4094
assert_failure "Pulse VLAN zero rejected" TAPM_PULSE_VALID_OPTIONAL_VLAN 0
assert_success "blank Pulse VLAN accepted" TAPM_PULSE_VALID_OPTIONAL_VLAN ''
assert_success "12-character Pulse admin password accepted" \
TAPM_PULSE_VALID_ADMIN_PASSWORD '123456789012'
assert_success "72-byte Pulse admin password accepted" \
TAPM_PULSE_VALID_ADMIN_PASSWORD \
'123456789012345678901234567890123456789012345678901234567890123456789012'
assert_failure "short Pulse admin password rejected" \
TAPM_PULSE_VALID_ADMIN_PASSWORD '12345678901'
assert_failure "multibyte Pulse admin password still requires 12 characters" \
TAPM_PULSE_VALID_ADMIN_PASSWORD 'éééééé'
assert_failure "Pulse admin password above bcrypt limit rejected" \
TAPM_PULSE_VALID_ADMIN_PASSWORD \
'1234567890123456789012345678901234567890123456789012345678901234567890123'
resources='[
{"type":"lxc","name":"pulse-a","tags":"tapm;pulse"},
{"type":"qemu","name":"unrelated"}
]'
assert_success "tagged Pulse LXC detected" TAPM_PULSE_RESOURCE_INSTALLED "$resources"
assert_success "legacy Pulse hostname detected" \
TAPM_PULSE_RESOURCE_INSTALLED '[{"type":"lxc","name":"Pulse"}]'
assert_success "untagged default Pulse hostname detected" \
TAPM_PULSE_RESOURCE_INSTALLED '[{"type":"lxc","name":"Pulse-Monitor"}]'
assert_failure "unrelated resource not detected" \
TAPM_PULSE_RESOURCE_INSTALLED '[{"type":"qemu","name":"pulse"}]'
test_pulse_config_detection() {
local fixture_root status
fixture_root="$(mktemp -d /tmp/tapm-pulse-configs.XXXXXX)" || return 1
mkdir -p "${fixture_root}/pve1/lxc" || return 1
printf '%s\n' \
'arch: amd64' \
'hostname: Pulse-Monitor' \
'memory: 2048' >"${fixture_root}/pve1/lxc/200.conf"
TAPM_PULSE_RESOURCE_INSTALLED_FROM_CONFIGS "$fixture_root"
status=$?
rm -rf -- "$fixture_root"
return "$status"
}
assert_success "Pulse LXC detected from shared Proxmox configuration" \
test_pulse_config_detection
assert_failure "missing Pulse configuration directory is not installed" \
TAPM_PULSE_RESOURCE_INSTALLED_FROM_CONFIGS /does/not/exist
ha_status=$'quorum OK\nmaster pve1 (active, Sat Jul 25 12:00:00 2026)\nlrm pve1 (active, Sat Jul 25 12:00:00 2026)'
assert_success "active Proxmox HA detected" TAPM_PULSE_HA_STATUS_ENABLED "$ha_status"
assert_failure "inactive Proxmox HA rejected" \
TAPM_PULSE_HA_STATUS_ENABLED $'quorum OK\nmaster pve1 (idle)'
finish_tests
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
TEST_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
# shellcheck source=../inc/rmm.inc
source "${TEST_ROOT}/inc/rmm.inc"
example_token='a81581f5-2e8c-465a-9eff-a094a75d5bbf'
example_url="https://prod.setup.itsupport247.net/linux/BareboneAgent/64/TA_Green_Bay-Technology_Arch_Corporate_Linux_Server_ITSPlatform_TKN${example_token}/RUN/setup"
actual_token="$(TAPM_RMM_TOKEN_FROM_URL "$example_url")"
if [[ "$actual_token" != "$example_token" ]]; then
printf 'FAIL: extracted %q, want %q\n' "$actual_token" "$example_token" >&2
exit 1
fi
if TAPM_RMM_TOKEN_FROM_URL 'https://prod.setup.itsupport247.net/linux/setup' >/dev/null; then
printf 'FAIL: accepted an RMM URL without a token\n' >&2
exit 1
fi
if TAPM_RMM_TOKEN_FROM_URL \
'https://prod.setup.itsupport247.net/TKNnot-a-token/RUN/setup' >/dev/null; then
printf 'FAIL: accepted a malformed RMM token\n' >&2
exit 1
fi
printf 'PASS: RMM token extraction\n'
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
test_dir="$(mktemp -d)"
trap 'rm -rf "$test_dir"' EXIT
TAPM_CONFIG_FILE="${test_dir}/etc/config.env"
TAPM_CONFIG_TEST_STDIN=1
source "${TEST_ROOT}/inc/runtime-config.inc"
if ! printf '%s\n%s\n' \
'https://tapm.example.com' \
'git.example.com' |
TAPM_ENSURE_RUNTIME_CONFIG >/dev/null; then
printf 'FAIL: configuration wizard failed\n' >&2
exit 1
fi
expected=$'TAPM_BROKER_URL=https://tapm.example.com\nGITEA_DOMAIN=git.example.com'
actual="$(cat "$TAPM_CONFIG_FILE")"
if [[ "$actual" != "$expected" ]]; then
printf 'FAIL: unexpected configuration contents\n' >&2
exit 1
fi
if stat -c '%a' "$TAPM_CONFIG_FILE" >/dev/null 2>&1; then
config_mode="$(stat -c '%a' "$TAPM_CONFIG_FILE")"
else
config_mode="$(stat -f '%Lp' "$TAPM_CONFIG_FILE")"
fi
if [[ "$config_mode" != 600 ]]; then
printf 'FAIL: configuration mode is not 600\n' >&2
exit 1
fi
unset TAPM_BROKER_URL GITEA_DOMAIN GITEA_URL
TAPM_LOAD_RUNTIME_CONFIG
if [[ "$TAPM_BROKER_URL" != 'https://tapm.example.com' ||
"$GITEA_DOMAIN" != 'git.example.com' ||
"$GITEA_URL" != 'https://git.example.com' ]]; then
printf 'FAIL: saved configuration did not reload\n' >&2
exit 1
fi
printf 'PASS: runtime configuration wizard\n'
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
TEST_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
# shellcheck source=../inc/secure-input.inc
source "${TEST_ROOT}/inc/secure-input.inc"
masked_value=''
output_file="$(mktemp)"
trap 'rm -f "$output_file"' EXIT
TAPM_READ_MASKED masked_value >"$output_file" <<'EOF'
TAPM-secret
EOF
if [[ "$masked_value" != 'TAPM-secret' ]]; then
printf 'FAIL: masked input did not preserve the entered value\n' >&2
exit 1
fi
if [[ "$(cat "$output_file")" != '***********' ]]; then
printf 'FAIL: masked input did not display one marker per character\n' >&2
exit 1
fi
printf 'PASS: masked secret input\n'
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "${TEST_ROOT}/tests/testlib.sh"
source "${TEST_ROOT}/colors.inc"
assert_equal '\e[96m' "${idsCL[LightCyan]}" \
"bundled foreground colors are available"
assert_equal '\e[103m' "${idsBG[LightYellow]}" \
"bundled background colors are available"
assert_equal '\e[1m' "${idsST[Bold]}" \
"bundled text styles are available"
assert_success "EXIT1 is bundled in TA-ProxMenu defaults" \
grep -q '^EXIT1()' "${TEST_ROOT}/defaults.inc"
assert_success "ENTER2CONTINUE is bundled in TA-ProxMenu defaults" \
grep -q '^ENTER2CONTINUE()' "${TEST_ROOT}/defaults.inc"
assert_failure "runtime launchers do not reference external iDS defaults" \
grep -E -q '/opt/idssys/defaults|iDS-Defaults' \
"${TEST_ROOT}/run.sh" "${TEST_ROOT}/proxmenu-scripts.sh"
finish_tests
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -u -o pipefail
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "${TEST_ROOT}/tests/testlib.sh"
source "${TEST_ROOT}/inc/virtio-helpers.inc"
assert_equal virtio-win.iso \
"$(TAPM_VIRTIO_FILENAME_FROM_URL \
'https://fedorapeople.org/stable/virtio-win.iso?download=1')" \
"stable source filename"
assert_equal virtio-win-0.1.285.iso \
"$(TAPM_VIRTIO_FILENAME_FROM_URL \
'https://fedorapeople.org/archive/virtio-win-0.1.285.iso')" \
"versioned source filename"
assert_failure "unexpected source filename rejected" \
TAPM_VIRTIO_FILENAME_FROM_URL \
'https://example.invalid/virtio-win-latest.iso'
assert_equal virtio-win-latest-0.1.285.iso \
"$(TAPM_VIRTIO_LABELED_FILENAME virtio-win-0.1.285.iso latest)" \
"stable local filename"
assert_equal virtio-win-server-2016-0.1.240.iso \
"$(TAPM_VIRTIO_LABELED_FILENAME \
virtio-win-0.1.240.iso server-2016)" \
"Server 2016 local filename"
assert_equal virtio-win-server-2008r2-0.1.172.iso \
"$(TAPM_VIRTIO_LABELED_FILENAME \
virtio-win-0.1.172.iso server-2008r2)" \
"Server 2008 R2 local filename"
assert_failure "unsafe local label rejected" \
TAPM_VIRTIO_LABELED_FILENAME virtio-win-0.1.285.iso '../latest'
assert_success "fresh VirtIO cache accepted" \
TAPM_VIRTIO_CACHE_VALID 1000 1500 86400 virtio-win-0.1.285.iso
assert_failure "expired VirtIO cache rejected" \
TAPM_VIRTIO_CACHE_VALID 1000 87400 86400 virtio-win-0.1.285.iso
assert_failure "future VirtIO cache rejected" \
TAPM_VIRTIO_CACHE_VALID 2000 1000 86400 virtio-win-0.1.285.iso
assert_failure "unsafe VirtIO cache filename rejected" \
TAPM_VIRTIO_CACHE_VALID 1000 1500 86400 ../../installer
finish_tests
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
TEST_ASSERTIONS=0
TEST_FAILURES=0
test_fail() {
printf 'FAIL: %s\n' "$1" >&2
(( TEST_FAILURES += 1 ))
}
assert_equal() {
local expected="$1"
local actual="$2"
local label="$3"
(( TEST_ASSERTIONS += 1 ))
if [[ "$actual" != "$expected" ]]; then
test_fail "${label}: expected '${expected}', received '${actual}'"
fi
}
assert_success() {
local label="$1"
shift
(( TEST_ASSERTIONS += 1 ))
"$@" || test_fail "${label}: expected success"
}
assert_failure() {
local label="$1"
shift
(( TEST_ASSERTIONS += 1 ))
if "$@"; then
test_fail "${label}: expected failure"
fi
}
finish_tests() {
if (( TEST_FAILURES > 0 )); then
printf '%d assertion(s), %d failure(s)\n' \
"$TEST_ASSERTIONS" "$TEST_FAILURES" >&2
return 1
fi
printf '%d assertion(s) passed\n' "$TEST_ASSERTIONS"
}