Files
openvpncertupdate/tests/test_dialogs.py
Vlad Doloman f7ce56a6d4 fix curses.newwin() crash on narrow terminals in TUI dialogs
show_confirm() sized its window purely from message content, with no
upper bound — a long line (the Cryptgeon password URL in the
post-issuance "send email?" prompt) could easily exceed a narrow
terminal's width (e.g. a serial console), making curses.newwin()
raise "curses function returned NULL" and crash the whole app.
show_cert_form() had the same unguarded newwin() call, just with a
fixed 13x62 size instead of content-driven.

Clamp both windows' height/width to the actual screen size, and wrap
newwin() itself in try/except as a last-resort fallback (declined
confirm / cancelled form) in case clamping still isn't enough.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 16:33:38 +03:00

155 lines
5.7 KiB
Python
Raw 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_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