#!/usr/bin/env bash
# tests/opnsense-set-interface-v4/run-tests.sh
#
# Harness for scripts/opnsense-set-interface-v4.sh + scripts/opnsense-set-iface-v4.php.
#
# OFFLINE. Touches no edge. It exercises the arg contract and the SAFETY PROPERTIES,
# which are the entire reason this script is allowed to exist: it writes an OPNsense
# base-interface config on a LIVE router whose DHCP and firewall rules are API-managed
# and would be destroyed by a wholesale config.xml replace.
#
# Same pinned properties as the v6 harness, plus the two this tool adds:
# 7. Only ipaddr/subnet/gateway may be touched on the interface node.
# 8. The GATEWAY half may add at most ONE gateway_item, count-guarded like the
# interface count -- a live router's routing table is not a place for surprises.
# ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDIR="$(cd "$HERE/../../scripts" && pwd)"
SH="$SDIR/opnsense-set-interface-v4.sh"
PHP="$SDIR/opnsense-set-iface-v4.php"
pass=0; fail=0
ok() { pass=$((pass+1)); }
bad() { fail=$((fail+1)); echo " FAIL: $1"; }
# 0. both exist and the shell half parses
[ -f "$SH" ] && ok || bad "opnsense-set-interface-v4.sh missing"
[ -f "$PHP" ] && ok || bad "opnsense-set-iface-v4.php missing"
bash -n "$SH" 2>/dev/null && ok || bad "opnsense-set-interface-v4.sh does not parse"
if command -v php >/dev/null 2>&1; then
php -l "$PHP" >/dev/null 2>&1 && ok || bad "opnsense-set-iface-v4.php does not parse (php -l)"
else
ok # documented: no php on this host; the edge parses it at run time
fi
# 1. arg contract -- every missing arg must fail, not default to something.
"$SH" >/dev/null 2>&1; [ $? -ne 0 ] && ok || bad "no args must fail"
"$SH" 192.168.1.1 >/dev/null 2>&1; [ $? -ne 0 ] && ok || bad "host only must fail"
"$SH" 192.168.1.1 lan >/dev/null 2>&1; [ $? -ne 0 ] && ok || bad "no address must fail"
"$SH" 192.168.1.1 lan 10.12.4.1 >/dev/null 2>&1; [ $? -ne 0 ] && ok || bad "no prefixlen must fail"
# 2. VALIDATION BEFORE THE ROUTER. A bad value must die locally -- it must never be
# shipped to a live edge to be rejected there.
out="$("$SH" 192.168.1.1 lan 2602:f3e2:f01:100::1 22 2>&1)"
printf '%s' "$out" | grep -q "not an IPv4 address" && ok || bad "a v6 address was not rejected locally"
out="$("$SH" 192.168.1.1 lan 10.12.4.999 22 2>&1)"
printf '%s' "$out" | grep -q "not an IPv4 address" && ok || bad "an out-of-range octet was not rejected locally"
out="$("$SH" 192.168.1.1 lan 10.12.4.1 33 2>&1)"
printf '%s' "$out" | grep -q "out of range" && ok || bad "prefixlen 33 was not rejected locally"
out="$("$SH" 192.168.1.1 lan 10.12.4.1 abc 2>&1)"
printf '%s' "$out" | grep -q "not a prefix length" && ok || bad "a non-numeric prefixlen was not rejected"
out="$("$SH" 192.168.1.1 wan 172.30.2.2 24 notanip 2>&1)"
printf '%s' "$out" | grep -q "not an IPv4 gateway" && ok || bad "a bad gateway was not rejected locally"
# 3. no key -> refuse. (Validation runs first, so use an otherwise-valid call.)
out="$(env -u OPNSENSE_SSH_KEY "$SH" 192.168.1.1 lan 10.12.4.1 22 2>&1)"
printf '%s' "$out" | grep -q 'OPNSENSE_SSH_KEY not set' && ok || bad "missing OPNSENSE_SSH_KEY was not refused"
# 4. DRY BY DEFAULT -- in BOTH halves.
grep -q '\-\-commit' "$SH" && ok || bad "the wrapper has no --commit flag"
grep -q '\-\-commit' "$PHP" && ok || bad "the php has no --commit flag"
grep -q 'DRY RUN -- nothing written, nothing applied' "$SH" && ok || bad "the wrapper does not announce its dry run"
grep -q 'DRY RUN -- nothing was written' "$PHP" && ok || bad "the php does not announce its dry run"
grep -q 'if (!\$commit)' "$PHP" && ok || bad "the php has no dry-run guard before the write"
# 5. IT MUST NEVER RENDER OR PUSH A config.xml -- the D-113 red line.
grep -qiE 'config\.xml\.tmpl|render-config|scp .*config\.xml|>[[:space:]]*/conf/config\.xml' "$SH" \
&& bad "the wrapper renders or pushes a config.xml -- THE FORBIDDEN CLOBBER PATH" || ok
grep -qE 'file_put_contents|fopen\(.*/conf/config\.xml' "$PHP" \
&& bad "the php writes config.xml directly instead of via the vendor's Config object" || ok
grep -q 'Config::getInstance()' "$PHP" && ok || bad "the php does not use OPNsense's own Config singleton"
grep -q 'use OPNsense\\Core\\Config;' "$PHP" && ok || bad "the php no longer imports OPNsense's Config"
# 6. THE STRUCTURAL GUARDS -- interface count, before and after a fresh read-back.
grep -q 'iface_count_before' "$PHP" && ok || bad "the interface-count guard is gone"
grep -q 'NOT SAVING' "$PHP" && ok || bad "the php no longer refuses to save on a count mismatch"
grep -q 'CONFIG DAMAGED' "$PHP" && ok || bad "the post-save count re-check is gone"
grep -q 'forceReload' "$PHP" && ok || bad "the php does not re-read from disk -- it would trust its own memory"
grep -q 'read-back mismatch' "$PHP" && ok || bad "the php does not verify what it actually wrote"
# 7. only ipaddr/subnet/gateway may be touched on the interface node.
touched="$(grep -oE '\$node->[a-zA-Z0-9_]+ =' "$PHP" | sort -u | sed 's/\$node->//; s/ =//')"
[ "$(printf '%s\n' "$touched" | grep -c .)" -eq 3 ] && ok \
|| bad "the php assigns to other than 3 interface fields: $(printf '%s' "$touched" | tr '\n' ' ')"
printf '%s\n' "$touched" | grep -qx 'ipaddr' && ok || bad "the php no longer sets ipaddr"
printf '%s\n' "$touched" | grep -qx 'subnet' && ok || bad "the php no longer sets subnet"
printf '%s\n' "$touched" | grep -qx 'gateway' && ok || bad "the php no longer sets the interface gateway"
grep -q 'UNTOUCHED' "$PHP" && ok || bad "the php no longer shows which fields it leaves alone"
# 8. THE GATEWAY HALF: count-guarded, at most one added item, and only when asked.
grep -q 'gw_count_before' "$PHP" && ok || bad "the gateway-count guard is gone"
grep -q 'expected +' "$PHP" && ok || bad "the php does not assert HOW MANY gateway items it added"
grep -q 'gw_added' "$PHP" && ok || bad "the php no longer tracks whether it added a gateway item"
grep -q "if (\$gwaddr !== '')" "$PHP" && ok || bad "the gateway half is not conditional -- it must be opt-in"
grep -q 'read-back gateway mismatch' "$PHP" && ok || bad "the php does not verify the gateway it wrote"
# 9. THE TCSH TRAP. root's shell on the edge is tcsh; a $(...) inside a quoted remote
# command dies QUIETLY. Every remote command must be fed to `sh -s`.
grep -q 'sh -s' "$SH" && ok || bad "remote commands are not fed to 'sh -s' -- the TCSH trap"
grep -qi 'tcsh' "$SH" && ok || bad "the tcsh trap is no longer documented"
# 10. saving config != applying it.
grep -q 'configctl interface reconfigure' "$SH" && ok || bad "the wrapper never applies the change"
grep -q 'NOT yet applied to the running kernel' "$PHP" && ok || bad "the php no longer warns that a save is not an apply"
# 11. GROUND TRUTH. The kernel decides, not the config file and not a self-report.
grep -q 'ifconfig' "$SH" && ok || bad "the wrapper never checks the KERNEL for the address"
grep -q 'GROUND TRUTH' "$SH" && ok || bad "the ground-truth read-back is gone"
grep -q 'is NOT on' "$SH" && ok || bad "the wrapper does not FAIL when the address is absent after reconfigure"
# 12. THE DEVICE IS MEASURED, NOT GUESSED. The v6 sibling hardcodes lan->vtnet0 /
# wan->vtnet1 via sed; that mapping is per-edge and must be read from the edge.
grep -q "s/lan/vtnet0/" "$SH" && bad "the device mapping is HARDCODED (lan->vtnet0) -- it must be measured per edge" || ok
grep -q 'measured from the edge' "$SH" && ok || bad "the wrapper does not state that it measured the device"
# 13. the D-113 amendment must stay cited, WITH its 26.7 re-measurement -- this tool
# only exists because the API cannot do this.
grep -q 'D-113' "$SH" && ok || bad "the wrapper no longer cites D-113 (why this is not the REST API)"
grep -q 'D-113' "$PHP" && ok || bad "the php no longer cites D-113"
grep -q '26.7' "$SH" && ok || bad "the 26.7 re-measurement is no longer cited in the wrapper"
grep -q '26.7' "$PHP" && ok || bad "the 26.7 re-measurement is no longer cited in the php"
grep -qi 'interfaces.php' "$SH" && ok || bad "the measured evidence (legacy /interfaces.php page) is no longer cited"
# 14. THE ORDERING WARNING. Re-addressing the LAN moves the interface you arrived on;
# losing that warning is how a future operator strands themselves.
grep -qi 'WAN first' "$SH" && ok || bad "the WAN-before-LAN ordering warning is gone"
echo
total=$((pass+fail))
if [ "$fail" -eq 0 ]; then echo "opnsense-set-interface-v4: $pass/$total PASS"; exit 0; fi
echo "opnsense-set-interface-v4: $fail/$total FAIL"; exit 1