diff --git a/test_enroll.py b/test_enroll.py index 504b2ac..c0d5eb0 100644 --- a/test_enroll.py +++ b/test_enroll.py @@ -13,6 +13,86 @@ def bash(code, *args): class EnrollTests(unittest.TestCase): + def run_ui(self, action, answer='', rc=0, tool='whiptail', rows=24): + lib = SOURCE.rsplit('\nmain\n', 1)[0] + lib = lib.replace('set -Eeuo pipefail', 'set -Eeo pipefail', 1) + lib = lib.replace("[[ $EUID == 0 ]] || fail 'Bitte auf dem Proxmox-Host als root starten.'", ':') + lib = lib.replace('for tool in pct timeout python3; do command -v "$tool" >/dev/null || fail "$tool fehlt auf dem Host."; done', ':') + with tempfile.TemporaryDirectory() as td: + p = Path(td) + mocks = r''' +exec 3<>"$1/terminal" +exec 5>"$1/dialog-args" +TERM=xterm +command() { + if [[ $1 == -v && ( $2 == whiptail || $2 == dialog ) ]]; then [[ $2 == $MOCK_TOOL ]]; + else builtin command "$@"; fi +} +stty() { printf '%s 100\n' "$MOCK_ROWS"; } +whiptail() { printf '%s\n' "$@" >&5; printf '%s' "$MOCK_ANSWER" >&4; return "$MOCK_RC"; } +dialog() { whiptail "$@"; } +init_ui +IDS=(101 103); NAMES[101]='first'; NAMES[103]='partial' +TS[101]=0; WZ[101]=0; TUN[101]=0 +TS[103]=1; WZ[103]=0; TUN[103]=1 +''' + setup = 'MOCK_TOOL=$2; MOCK_ROWS=$3; MOCK_RC=$4; MOCK_ANSWER=$5\n' + r = bash('UI_TEST_ARGS=("$@"); set --\n' + lib + '\nset -- "${UI_TEST_ARGS[@]}"\n' + + setup + mocks + action, td, tool, str(rows), str(rc), answer) + return r, (p/'dialog-args').read_text(), (p/'terminal').read_text() + + def test_checklist_captures_multiple_ids_and_shows_component_status(self): + r, args, terminal = self.run_ui('choose_lxcs; echo "SELECTION=$SELECTION"', '101\n103\n') + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn('SELECTION=101 103', r.stdout) + self.assertIn('--separate-output', args) + self.assertIn('--checklist', args) + self.assertIn('partial | Tailscale: vorhanden | Wazuh: fehlt', args) + self.assertEqual(args.splitlines().count('OFF'), 2) + + def test_cancel_or_empty_checklist_stops_selection(self): + for rc, answer in ((1, ''), (255, ''), (0, '')): + r, args, terminal = self.run_ui( + 'if choose_lxcs; then echo CONTINUE; else echo CANCEL; fi', answer, rc) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn('CANCEL', r.stdout) + self.assertNotIn('CONTINUE', r.stdout) + + def test_yes_no_dialog_preserves_default_and_handles_escape(self): + r, args, terminal = self.run_ui('yes_no "Sudo? [j/N]" 0; echo "ANSWER=$REPLY"', rc=1) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn('ANSWER=0', r.stdout) + self.assertIn('--defaultno', args) + self.assertNotIn('[j/N]', args) + r, args, terminal = self.run_ui('yes_no "SSH? [J/n]" 1; echo "ANSWER=$REPLY"') + self.assertIn('ANSWER=1', r.stdout) + self.assertNotIn('--defaultno', args) + r, args, terminal = self.run_ui('yes_no "SSH?" 1; echo CONTINUE', rc=255) + self.assertNotEqual(r.returncode, 0) + self.assertNotIn('CONTINUE', r.stdout) + + def test_password_dialog_does_not_echo_secret_or_put_it_in_arguments(self): + r, args, terminal = self.run_ui('secret "Key:"; echo "LENGTH=${#REPLY}"', 'fixture-private-key') + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn('--passwordbox', args) + self.assertNotIn('--insecure', args) + self.assertNotIn('fixture-private-key', r.stdout + r.stderr + args + terminal) + + def test_input_dialog_and_dialog_fallback(self): + r, args, terminal = self.run_ui('ask "Nutzer:" "existing"; echo "INPUT=$REPLY"', 'chosen', tool='dialog') + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn('--inputbox', args) + self.assertIn('existing', args) + self.assertIn('INPUT=chosen', r.stdout) + + def test_no_tool_or_small_terminal_uses_text_without_installing_anything(self): + for tool, rows in (('none', 24), ('whiptail', 12)): + r, args, terminal = self.run_ui( + 'ask() { REPLY="101 103"; }; choose_lxcs; echo "SELECTION=$SELECTION"', tool=tool, rows=rows) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn('SELECTION=101 103', r.stdout) + self.assertEqual(args, '') + def test_syntax(self): for code in (SOURCE, WORKER): r = subprocess.run(['bash', '-n'], input=code, text=True, capture_output=True)