Files
openvpncertupdate/tests/test_dialogs.py
Vlad Doloman 2640dbf81d use erase() instead of clear() in per-keystroke TUI redraw loops
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>
2026-07-08 17:45:13 +03:00

167 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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