"""Tests for TUI DIALOGS (show_confirm, show_cert_form). Uses the same mock-curses approach as test_widgets.py — no real terminal needed. """ from unittest.mock import MagicMock, patch import curses as _curses # Stub curses constants before importing the module under test, mirroring # what test_widgets.py does for InputField's dependencies. _curses.color_pair = lambda x: 0 _curses.curs_set = lambda x: None _curses.A_UNDERLINE = 0 _curses.A_BOLD = 0 _curses.KEY_BACKSPACE = 263 _curses.KEY_LEFT = 260 _curses.KEY_RIGHT = 261 _curses.KEY_HOME = 262 _curses.KEY_END = 360 _curses.KEY_DC = 330 _curses.KEY_BTAB = 353 _curses.KEY_F5 = 269 _curses.KEY_ENTER = 343 from openvpncertupdate import show_confirm, show_cert_form, CertFormResult def _make_stdscr(rows=40, cols=120): """Return a minimal MagicMock that looks like a curses stdscr.""" stdscr = MagicMock() stdscr.getmaxyx.return_value = (rows, cols) return stdscr def _make_win(rows=20, cols=60): """Return a minimal MagicMock for the subwindow created by curses.newwin.""" win = MagicMock() win.getmaxyx.return_value = (rows, cols) return win # --------------------------------------------------------------------------- # show_confirm tests # --------------------------------------------------------------------------- def test_show_confirm_yes(): """Pressing 'y' returns True.""" stdscr = _make_stdscr() win = _make_win() win.getch.side_effect = [ord("y")] with patch("curses.newwin", return_value=win): result = show_confirm(stdscr, "Do you want to continue?") assert result is True def test_show_confirm_esc(): """Pressing Escape (27) returns False.""" stdscr = _make_stdscr() win = _make_win() win.getch.side_effect = [27] with patch("curses.newwin", return_value=win): result = show_confirm(stdscr, "Do you want to continue?") assert result is False def test_show_confirm_clamps_width_to_screen_for_long_line(): """A long message line (e.g. a Cryptgeon password URL) must not make the requested newwin() width exceed the screen — that's what made curses.newwin() raise 'curses function returned NULL' on a narrow terminal such as a serial console.""" stdscr = _make_stdscr(rows=24, cols=80) win = _make_win() win.getch.side_effect = [ord("y")] long_url = "https://cryptgeon.example.com/note/" + "a" * 100 + "#" + "b" * 64 with patch("curses.newwin", return_value=win) as mock_newwin: show_confirm(stdscr, f"Send email?\n\nPassword URL: {long_url}") h, w, y, x = mock_newwin.call_args.args assert w <= 80 assert x >= 0 def test_show_confirm_newwin_failure_returns_false(): """If curses.newwin() still fails despite clamping, show_confirm must degrade to 'declined' rather than crash the whole app.""" stdscr = _make_stdscr() with patch("curses.newwin", side_effect=_curses.error("returned NULL")): result = show_confirm(stdscr, "Do you want to continue?") assert result is False # --------------------------------------------------------------------------- # show_cert_form tests # --------------------------------------------------------------------------- def test_show_cert_form_cancel(): """Pressing Escape returns a CertFormResult with confirmed=False.""" stdscr = _make_stdscr() win = _make_win(rows=20, cols=70) win.getch.side_effect = [27] with patch("curses.newwin", return_value=win): result = show_cert_form(stdscr, cn="alice", email="alice@example.com") assert isinstance(result, CertFormResult) assert result.confirmed is False def test_show_cert_form_uses_erase_not_clear(): # clear() forces a full-screen repaint on every refresh(); the form # redraws on every keystroke, so it must use erase() instead. stdscr = _make_stdscr() win = _make_win(rows=20, cols=70) win.getch.side_effect = [9, 27] # Tab, then Escape with patch("curses.newwin", return_value=win): show_cert_form(stdscr, cn="alice", email="alice@example.com") assert win.erase.called assert not win.clear.called def test_show_cert_form_confirm(): """Enter advances through the 4 fields to the Continue button; Enter on Continue confirms. That's 5 Enter keypresses total. days="90" (non-blank) is used deliberately: a blank default would still read back as "" even from a reverted three-field form (CertFormResult.days defaults to ""), so it would not actually prove the days field was traversed. A non-blank value only round-trips if the days field exists, was reached by the Enter sequence, and was carried into the result.""" stdscr = _make_stdscr() win = _make_win(rows=20, cols=70) win.getch.side_effect = [10, 10, 10, 10, 10] with patch("curses.newwin", return_value=win): result = show_cert_form(stdscr, cn="bob", email="bob@example.com", days="90") assert isinstance(result, CertFormResult) assert result.confirmed is True assert result.cn == "bob" assert result.email == "bob@example.com" assert result.days == "90" assert len(result.password) > 0 # Pins the field count itself: a partial reversion that drops "days" # from _FORM_FIELDS while CertFormResult/_submit() keep it would still # round-trip days="90" above (it's passed straight through, untraversed), # but would only consume 4 Enters, not 5. assert win.getch.call_count == 5 def test_show_cert_form_cancel_button(): """Tab to the Cancel button (5 Tabs from start) and press Enter cancels.""" stdscr = _make_stdscr() win = _make_win(rows=20, cols=70) # Tab×5: cn→email→days→password→Continue→Cancel, then Enter win.getch.side_effect = [9, 9, 9, 9, 9, 10] with patch("curses.newwin", return_value=win): result = show_cert_form(stdscr, cn="alice", email="alice@example.com") assert isinstance(result, CertFormResult) assert result.confirmed is False def test_show_cert_form_clamps_to_narrow_screen(): """The form's fixed 15x62 size must not exceed a smaller-than-usual screen, which would make curses.newwin() raise.""" stdscr = _make_stdscr(rows=10, cols=40) win = _make_win() win.getch.side_effect = [27] with patch("curses.newwin", return_value=win) as mock_newwin: show_cert_form(stdscr, cn="alice", email="alice@example.com") h, w, y, x = mock_newwin.call_args.args assert h <= 10 assert w <= 40 assert y >= 0 assert x >= 0 def test_show_cert_form_returns_days(): stdscr = _make_stdscr() win = _make_win(rows=20, cols=70) win.getch.side_effect = [10, 10, 10, 10, 10] with patch("curses.newwin", return_value=win): result = show_cert_form(stdscr, cn="bob", email="b@c.d", days="90") assert result.confirmed is True assert result.days == "90" def test_show_cert_form_blank_days_means_inherit(): stdscr = _make_stdscr() win = _make_win(rows=20, cols=70) win.getch.side_effect = [10, 10, 10, 10, 10] with patch("curses.newwin", return_value=win): result = show_cert_form(stdscr, cn="bob", email="b@c.d", days="") assert result.confirmed is True assert result.days == "" def test_show_cert_form_days_field_is_digits_only(): # cn is read-only here, so focus starts on email: Tab once to reach days. stdscr = _make_stdscr() win = _make_win(rows=20, cols=70) win.getch.side_effect = [9] + [ord(c) for c in "9a0!"] + [10, 10, 10] with patch("curses.newwin", return_value=win): result = show_cert_form(stdscr, cn="bob", email="b@c.d", days="", cn_readonly=True) assert result.confirmed is True assert result.days == "90" def test_show_cert_form_refuses_zero_days(): # Submitting "0" must keep the form open rather than closing and failing # later inside EasyRSA. Enter×5 tries to submit, Esc then cancels. stdscr = _make_stdscr() win = _make_win(rows=20, cols=70) win.getch.side_effect = [10, 10, 10, 10, 10, 27] with patch("curses.newwin", return_value=win): result = show_cert_form(stdscr, cn="bob", email="b@c.d", days="0") assert result.confirmed is False def test_show_cert_form_normalises_leading_zero_days(): stdscr = _make_stdscr() win = _make_win(rows=20, cols=70) win.getch.side_effect = [10, 10, 10, 10, 10] with patch("curses.newwin", return_value=win): result = show_cert_form(stdscr, cn="bob", email="b@c.d", days="090") assert result.days == "90" def test_show_cert_form_newwin_failure_returns_cancelled(): """If curses.newwin() still fails despite clamping, show_cert_form must degrade to an unconfirmed result rather than crash the whole app.""" stdscr = _make_stdscr() with patch("curses.newwin", side_effect=_curses.error("returned NULL")): result = show_cert_form(stdscr, cn="alice", email="alice@example.com") assert isinstance(result, CertFormResult) assert result.confirmed is False