clear() forces a full physical screen repaint on the next refresh(); erase() just blanks the buffer content and lets curses diff against what's already on the terminal, sending only the changed cells. The main cert list and the cert-entry form both redraw on every keystroke, so clear() there meant retransmitting the whole screen on every arrow key / typed character — slow and flickery over a low-bandwidth link like a serial console. One-shot dialogs (show_confirm, _msg, _error) draw once and don't loop-redraw, so they keep clear() as-is — no benefit to changing them and it's one less thing to get wrong. Also handle curses.KEY_RESIZE explicitly in the main list loop: force one real clear() right after a detected resize, since erase()'s diff against the pre-resize screen model isn't reliable across a genuine dimension change. Screen-size changes were already picked up on the next redraw before this (draw() runs unconditionally every loop iteration), this just guarantees a clean repaint at the moment of the actual resize instead of trusting the diff engine through it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
167 lines
6.2 KiB
Python
167 lines
6.2 KiB
Python
"""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 3 fields to the Continue button; Enter on
|
||
Continue confirms. That's 4 Enter keypresses total."""
|
||
stdscr = _make_stdscr()
|
||
win = _make_win(rows=20, cols=70)
|
||
win.getch.side_effect = [10, 10, 10, 10]
|
||
with patch("curses.newwin", return_value=win):
|
||
result = show_cert_form(stdscr, cn="bob", email="bob@example.com")
|
||
assert isinstance(result, CertFormResult)
|
||
assert result.confirmed is True
|
||
assert result.cn == "bob"
|
||
assert result.email == "bob@example.com"
|
||
assert len(result.password) > 0
|
||
|
||
|
||
def test_show_cert_form_cancel_button():
|
||
"""Tab to the Cancel button (4 Tabs from start) and press Enter cancels."""
|
||
stdscr = _make_stdscr()
|
||
win = _make_win(rows=20, cols=70)
|
||
# Tab×4: cn→email→password→Continue→Cancel, then Enter
|
||
win.getch.side_effect = [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 13x62 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_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
|