Add interactive menu regression
This commit is contained in:
@@ -143,6 +143,7 @@ tests/run_all.sh --skip-integration
|
|||||||
测试覆盖:
|
测试覆盖:
|
||||||
|
|
||||||
- `tests/test_cli.sh`:入口脚本与安装脚本帮助输出
|
- `tests/test_cli.sh`:入口脚本与安装脚本帮助输出
|
||||||
|
- `tests/test_interactive.sh`:交互主菜单的 add/list/delete/save 回归
|
||||||
- `tests/test_common.sh`:输入校验
|
- `tests/test_common.sh`:输入校验
|
||||||
- `tests/test_storage.sh`:规则存储
|
- `tests/test_storage.sh`:规则存储
|
||||||
- `tests/test_env_check.sh`:环境检查与修复
|
- `tests/test_env_check.sh`:环境检查与修复
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ main_menu_loop() {
|
|||||||
local choice
|
local choice
|
||||||
while true; do
|
while true; do
|
||||||
render_main_menu
|
render_main_menu
|
||||||
choice=$(prompt_input '请选择 [0-5]' '')
|
prompt_input_capture '请选择 [0-5]' ''
|
||||||
|
choice=${PROMPT_INPUT_RESULT}
|
||||||
case ${choice} in
|
case ${choice} in
|
||||||
1) cmd_list ;;
|
1) cmd_list ;;
|
||||||
2) cmd_add ;;
|
2) cmd_add ;;
|
||||||
|
|||||||
@@ -150,17 +150,16 @@ print_section() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
read_test_input() {
|
read_test_input() {
|
||||||
local reply=''
|
READ_TEST_INPUT_RESULT=''
|
||||||
if [[ -n ${IPF_TEST_INPUTS-} ]]; then
|
if [[ -n ${IPF_TEST_INPUTS-} ]]; then
|
||||||
if [[ ${IPF_TEST_INPUTS} == *$'\n'* ]]; then
|
if [[ ${IPF_TEST_INPUTS} == *$'\n'* ]]; then
|
||||||
reply=${IPF_TEST_INPUTS%%$'\n'*}
|
READ_TEST_INPUT_RESULT=${IPF_TEST_INPUTS%%$'\n'*}
|
||||||
IPF_TEST_INPUTS=${IPF_TEST_INPUTS#*$'\n'}
|
IPF_TEST_INPUTS=${IPF_TEST_INPUTS#*$'\n'}
|
||||||
else
|
else
|
||||||
reply=${IPF_TEST_INPUTS}
|
READ_TEST_INPUT_RESULT=${IPF_TEST_INPUTS}
|
||||||
IPF_TEST_INPUTS=''
|
IPF_TEST_INPUTS=''
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
printf '%s' "${reply}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt_input() {
|
prompt_input() {
|
||||||
@@ -169,10 +168,12 @@ prompt_input() {
|
|||||||
local reply=''
|
local reply=''
|
||||||
|
|
||||||
if [[ -n ${IPF_TEST_INPUTS-} ]]; then
|
if [[ -n ${IPF_TEST_INPUTS-} ]]; then
|
||||||
reply=$(read_test_input)
|
read_test_input
|
||||||
|
reply=${READ_TEST_INPUT_RESULT}
|
||||||
if [[ -z ${reply} && -n ${default_value} ]]; then
|
if [[ -z ${reply} && -n ${default_value} ]]; then
|
||||||
reply=${default_value}
|
reply=${default_value}
|
||||||
fi
|
fi
|
||||||
|
PROMPT_INPUT_RESULT=${reply}
|
||||||
printf '%s\n' "${reply}" >&2
|
printf '%s\n' "${reply}" >&2
|
||||||
printf '%s\n' "${reply}"
|
printf '%s\n' "${reply}"
|
||||||
return 0
|
return 0
|
||||||
@@ -184,9 +185,14 @@ prompt_input() {
|
|||||||
else
|
else
|
||||||
read -r -p "${prompt}: " reply
|
read -r -p "${prompt}: " reply
|
||||||
fi
|
fi
|
||||||
|
PROMPT_INPUT_RESULT=${reply}
|
||||||
printf '%s\n' "${reply}"
|
printf '%s\n' "${reply}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prompt_input_capture() {
|
||||||
|
prompt_input "$@" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
prompt_confirm() {
|
prompt_confirm() {
|
||||||
local prompt=$1
|
local prompt=$1
|
||||||
local default_choice=${2:-n}
|
local default_choice=${2:-n}
|
||||||
@@ -198,7 +204,8 @@ prompt_confirm() {
|
|||||||
suffix='[y/N]'
|
suffix='[y/N]'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
answer=$(prompt_input "${prompt} ${suffix}" "")
|
prompt_input_capture "${prompt} ${suffix}" ""
|
||||||
|
answer=${PROMPT_INPUT_RESULT}
|
||||||
if [[ -z ${answer} ]]; then
|
if [[ -z ${answer} ]]; then
|
||||||
answer=${default_choice}
|
answer=${default_choice}
|
||||||
fi
|
fi
|
||||||
@@ -218,15 +225,21 @@ prompt_select() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
answer=$(prompt_input "${prompt}" "")
|
prompt_input_capture "${prompt}" ""
|
||||||
|
answer=${PROMPT_INPUT_RESULT}
|
||||||
if [[ ${answer} =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= count )); then
|
if [[ ${answer} =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= count )); then
|
||||||
printf '%s\n' "$((answer - 1))"
|
PROMPT_SELECT_RESULT=$((answer - 1))
|
||||||
|
printf '%s\n' "${PROMPT_SELECT_RESULT}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
log_warn "请输入 1-${count} 之间的编号。"
|
log_warn "请输入 1-${count} 之间的编号。"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prompt_select_capture() {
|
||||||
|
prompt_select "$@" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
pause_return() {
|
pause_return() {
|
||||||
if [[ -n ${IPF_TEST_INPUTS-} ]]; then
|
if [[ -n ${IPF_TEST_INPUTS-} ]]; then
|
||||||
return 0
|
return 0
|
||||||
@@ -272,7 +285,7 @@ _validate_ipv6_part_count() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
[[ ${field} =~ ^[0-9A-Fa-f]{1,4}$ ]] || return 1
|
[[ ${field} =~ ^[0-9A-Fa-f]{1,4}$ ]] || return 1
|
||||||
((count++))
|
((count += 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
printf '%s\n' "${count}"
|
printf '%s\n' "${count}"
|
||||||
|
|||||||
@@ -216,7 +216,8 @@ cmd_add() {
|
|||||||
local proto_choice ipver_choice proto ipver lport tip tport desc
|
local proto_choice ipver_choice proto ipver lport tip tport desc
|
||||||
|
|
||||||
print_section '添加新的转发规则'
|
print_section '添加新的转发规则'
|
||||||
proto_choice=$(prompt_select '请选择协议' 'TCP' 'UDP' 'TCP + UDP')
|
prompt_select_capture '请选择协议' 'TCP' 'UDP' 'TCP + UDP'
|
||||||
|
proto_choice=${PROMPT_SELECT_RESULT}
|
||||||
case ${proto_choice} in
|
case ${proto_choice} in
|
||||||
0) proto=tcp ;;
|
0) proto=tcp ;;
|
||||||
1) proto=udp ;;
|
1) proto=udp ;;
|
||||||
@@ -225,12 +226,14 @@ cmd_add() {
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
lport=$(prompt_input '请输入本地监听端口' '')
|
prompt_input_capture '请输入本地监听端口' ''
|
||||||
|
lport=${PROMPT_INPUT_RESULT}
|
||||||
validate_port "${lport}" && break
|
validate_port "${lport}" && break
|
||||||
log_warn '端口无效,请重新输入。'
|
log_warn '端口无效,请重新输入。'
|
||||||
done
|
done
|
||||||
|
|
||||||
ipver_choice=$(prompt_select '请选择 IP 版本' '仅 IPv4' '仅 IPv6' '同时 IPv4 + IPv6')
|
prompt_select_capture '请选择 IP 版本' '仅 IPv4' '仅 IPv6' '同时 IPv4 + IPv6'
|
||||||
|
ipver_choice=${PROMPT_SELECT_RESULT}
|
||||||
case ${ipver_choice} in
|
case ${ipver_choice} in
|
||||||
0) ipver=4 ;;
|
0) ipver=4 ;;
|
||||||
1) ipver=6 ;;
|
1) ipver=6 ;;
|
||||||
@@ -239,18 +242,21 @@ cmd_add() {
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
tip=$(prompt_input "$(rule_target_hint "${ipver}")" '')
|
prompt_input_capture "$(rule_target_hint "${ipver}")" ''
|
||||||
|
tip=${PROMPT_INPUT_RESULT}
|
||||||
rule_validate_target "${tip}" "${ipver}" && break
|
rule_validate_target "${tip}" "${ipver}" && break
|
||||||
log_warn '目标地址无效,请重新输入。'
|
log_warn '目标地址无效,请重新输入。'
|
||||||
done
|
done
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
tport=$(prompt_input '请输入目标端口' '')
|
prompt_input_capture '请输入目标端口' ''
|
||||||
|
tport=${PROMPT_INPUT_RESULT}
|
||||||
validate_port "${tport}" && break
|
validate_port "${tport}" && break
|
||||||
log_warn '目标端口无效,请重新输入。'
|
log_warn '目标端口无效,请重新输入。'
|
||||||
done
|
done
|
||||||
|
|
||||||
desc=$(prompt_input '请输入描述(可留空)' '')
|
prompt_input_capture '请输入描述(可留空)' ''
|
||||||
|
desc=${PROMPT_INPUT_RESULT}
|
||||||
desc=$(rule_sanitize_desc "${desc}")
|
desc=$(rule_sanitize_desc "${desc}")
|
||||||
|
|
||||||
printf '协议: %s\n' "$(rule_proto_label "${proto}")"
|
printf '协议: %s\n' "$(rule_proto_label "${proto}")"
|
||||||
@@ -319,7 +325,8 @@ cmd_delete() {
|
|||||||
cmd_list 0
|
cmd_list 0
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
answer=$(prompt_input '请输入要删除的规则编号' '')
|
prompt_input_capture '请输入要删除的规则编号' ''
|
||||||
|
answer=${PROMPT_INPUT_RESULT}
|
||||||
if [[ ${answer} =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= ${#RULES_CACHE[@]} )); then
|
if [[ ${answer} =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= ${#RULES_CACHE[@]} )); then
|
||||||
index=$((answer - 1))
|
index=$((answer - 1))
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ storage_count() {
|
|||||||
local count=0 line
|
local count=0 line
|
||||||
while IFS= read -r line || [[ -n ${line} ]]; do
|
while IFS= read -r line || [[ -n ${line} ]]; do
|
||||||
[[ -n ${line} ]] || continue
|
[[ -n ${line} ]] || continue
|
||||||
((count++))
|
((count += 1))
|
||||||
done < <(storage_list)
|
done < <(storage_list)
|
||||||
printf '%s\n' "${count}"
|
printf '%s\n' "${count}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ run_test() {
|
|||||||
|
|
||||||
run_test "${ROOT_DIR}/tests/test_common.sh"
|
run_test "${ROOT_DIR}/tests/test_common.sh"
|
||||||
run_test "${ROOT_DIR}/tests/test_cli.sh"
|
run_test "${ROOT_DIR}/tests/test_cli.sh"
|
||||||
|
run_test "${ROOT_DIR}/tests/test_interactive.sh"
|
||||||
run_test "${ROOT_DIR}/tests/test_storage.sh"
|
run_test "${ROOT_DIR}/tests/test_storage.sh"
|
||||||
run_test "${ROOT_DIR}/tests/test_env_check.sh"
|
run_test "${ROOT_DIR}/tests/test_env_check.sh"
|
||||||
run_test "${ROOT_DIR}/tests/test_rules_unit.sh"
|
run_test "${ROOT_DIR}/tests/test_rules_unit.sh"
|
||||||
|
|||||||
@@ -15,6 +15,17 @@ status_of() {
|
|||||||
printf '%s\n' "${rc}"
|
printf '%s\n' "${rc}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prompt_tmp=$(mktemp)
|
||||||
|
trap 'rm -f "${prompt_tmp}"' EXIT
|
||||||
|
|
||||||
|
export IPF_TEST_INPUTS=$'first\nsecond\n'
|
||||||
|
prompt_input 'first prompt' '' >"${prompt_tmp}"
|
||||||
|
assert_eq 'first' "$(cat "${prompt_tmp}")" 'prompt_input should consume the first queued test input'
|
||||||
|
prompt_input 'second prompt' '' >"${prompt_tmp}"
|
||||||
|
assert_eq 'second' "$(cat "${prompt_tmp}")" 'prompt_input should consume subsequent queued test inputs'
|
||||||
|
assert_eq '' "${IPF_TEST_INPUTS}" 'prompt_input should drain queued test inputs in the current shell'
|
||||||
|
unset IPF_TEST_INPUTS
|
||||||
|
|
||||||
assert_status 0 "$(status_of validate_ipv4 '0.0.0.0')" 'validate_ipv4 should accept 0.0.0.0'
|
assert_status 0 "$(status_of validate_ipv4 '0.0.0.0')" 'validate_ipv4 should accept 0.0.0.0'
|
||||||
assert_status 0 "$(status_of validate_ipv4 '192.168.1.1')" 'validate_ipv4 should accept private address'
|
assert_status 0 "$(status_of validate_ipv4 '192.168.1.1')" 'validate_ipv4 should accept private address'
|
||||||
assert_status 0 "$(status_of validate_ipv4 '255.255.255.255')" 'validate_ipv4 should accept broadcast'
|
assert_status 0 "$(status_of validate_ipv4 '255.255.255.255')" 'validate_ipv4 should accept broadcast'
|
||||||
|
|||||||
62
tests/test_interactive.sh
Normal file
62
tests/test_interactive.sh
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
# shellcheck source=tests/lib/assert.sh
|
||||||
|
source "${ROOT_DIR}/tests/lib/assert.sh"
|
||||||
|
|
||||||
|
maybe_enter_namespace() {
|
||||||
|
if (( EUID == 0 )); then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${IPF_IN_NAMESPACE:-0} == 1 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v unshare >/dev/null 2>&1 && unshare -Urn true >/dev/null 2>&1; then
|
||||||
|
exec unshare -Urn env IPF_IN_NAMESPACE=1 bash "$0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'SKIP: 交互测试需要 root 或可用的 unshare。\n'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_enter_namespace
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||||
|
|
||||||
|
export IPF_STORAGE_DIR="${TMP_DIR}/storage"
|
||||||
|
export IPF_STORAGE_DB="${IPF_STORAGE_DIR}/rules.db"
|
||||||
|
export IPF_LOCK_FILE="${IPF_STORAGE_DIR}/.lock"
|
||||||
|
export IPTABLES_MOCK_DIR="${TMP_DIR}/iptables-mock"
|
||||||
|
export IPTABLES_MOCK_LOG="${TMP_DIR}/iptables.log"
|
||||||
|
export IPTABLES_BIN="${ROOT_DIR}/tests/mocks/iptables"
|
||||||
|
export IP6TABLES_BIN="${ROOT_DIR}/tests/mocks/ip6tables"
|
||||||
|
export IPTABLES_SAVE_BIN="${ROOT_DIR}/tests/mocks/iptables-save"
|
||||||
|
export IP6TABLES_SAVE_BIN="${ROOT_DIR}/tests/mocks/ip6tables-save"
|
||||||
|
export NETFILTER_PERSISTENT_BIN="${ROOT_DIR}/tests/mocks/persist-mock.sh"
|
||||||
|
export PERSIST_MOCK_LOG="${TMP_DIR}/persist.log"
|
||||||
|
export IPF_SKIP_ENV_CHECK=1
|
||||||
|
export IPF_FORCE_PLAIN_UI=1
|
||||||
|
export IPF_TEST_INPUTS=$'4\n2\n1\n8080\n1\n127.0.0.1\n80\ninteractive web\ny\n5\n1\n3\n1\ny\n0\n'
|
||||||
|
|
||||||
|
output=$("${ROOT_DIR}/iptables-forward.sh" 2>&1)
|
||||||
|
|
||||||
|
assert_contains "${output}" '== 环境状态 ==' 'interactive flow should open environment status view'
|
||||||
|
assert_contains "${output}" '规则总数: 0' 'environment status should show empty rule count before adding'
|
||||||
|
assert_contains "${output}" '== 添加新的转发规则 ==' 'interactive flow should enter add screen'
|
||||||
|
assert_contains "${output}" '描述: interactive web' 'interactive add summary should echo description'
|
||||||
|
assert_contains "${output}" '规则已保存到磁盘。' 'interactive flow should exercise menu save action'
|
||||||
|
assert_contains "${output}" 'interactive web' 'interactive list/delete flow should display the created description'
|
||||||
|
assert_contains "${output}" '规则已添加,UUID=' 'interactive flow should add one managed rule'
|
||||||
|
assert_contains "${output}" '已退出。' 'interactive flow should return cleanly from main menu'
|
||||||
|
|
||||||
|
assert_eq '0' "$(wc -l < "${IPF_STORAGE_DB}")" 'interactive add/delete flow should leave storage empty'
|
||||||
|
assert_eq '0' "$(wc -l < "${IPTABLES_MOCK_DIR}/state.v4")" 'interactive add/delete flow should leave runtime mock state empty'
|
||||||
|
assert_eq '3' "$(grep -Ec ' -A ' "${IPTABLES_MOCK_LOG}")" 'interactive add flow should emit three IPv4 add commands'
|
||||||
|
assert_eq '3' "$(grep -Ec ' -D ' "${IPTABLES_MOCK_LOG}")" 'interactive delete should emit three delete commands'
|
||||||
|
assert_eq '3' "$(grep -Ec 'persist-mock\.sh save' "${PERSIST_MOCK_LOG}")" 'interactive add/menu-save/delete flow should persist three times'
|
||||||
|
|
||||||
|
pass 'test_interactive.sh'
|
||||||
Reference in New Issue
Block a user