from unittest.mock import MagicMock, patch import curses as _curses # Stub just enough of the curses API for InputField to import _curses.color_pair = lambda x: 0 _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 from openvpncertupdate import InputField, clamp, init_colors, COLOR_NORMAL, COLOR_DISABLED def _field(initial="", mask=False): win = MagicMock() win.getmaxyx.return_value = (1, 80) return InputField(win, 0, 0, 40, initial=initial, mask=mask) def test_initial_value(): assert _field("hello").value == "hello" def test_backspace_removes_last(): f = _field("hello") f.handle_key(_curses.KEY_BACKSPACE) assert f.value == "hell" def test_type_appends(): f = _field("hi") f.handle_key(ord("!")) assert f.value == "hi!" def test_left_then_insert(): f = _field("ac") f.handle_key(_curses.KEY_LEFT) f.handle_key(_curses.KEY_LEFT) f.handle_key(ord("b")) assert f.value == "bac" def test_delete_key(): f = _field("abc") f.handle_key(_curses.KEY_HOME) f.handle_key(_curses.KEY_DC) assert f.value == "bc" def test_clamp_bounds(): assert clamp(5, 0, 10) == 5 assert clamp(-1, 0, 10) == 0 assert clamp(11, 0, 10) == 10 def _init_colors_calls(*, use_default_colors_raises, colors): calls = [] use_default = MagicMock( side_effect=_curses.error("use_default_colors() returned ERR") if use_default_colors_raises else None ) with patch.object(_curses, "start_color"), \ patch.object(_curses, "use_default_colors", use_default), \ patch.object(_curses, "init_pair", side_effect=lambda *a: calls.append(a)), \ patch.object(_curses, "COLORS", colors, create=True): init_colors() return calls def test_init_colors_uses_default_colors_when_supported(): calls = _init_colors_calls(use_default_colors_raises=False, colors=256) normal = next(c for c in calls if c[0] == COLOR_NORMAL) disabled = next(c for c in calls if c[0] == COLOR_DISABLED) assert normal[2] == -1 assert disabled[1:] == (8, -1) def test_init_colors_falls_back_on_serial_console(): # Serial consoles (e.g. TERM=linux/vt100) often lack the terminfo # "op"/"AX" capability, so use_default_colors() raises curses.error # and only 8 colors are available. calls = _init_colors_calls(use_default_colors_raises=True, colors=8) normal = next(c for c in calls if c[0] == COLOR_NORMAL) disabled = next(c for c in calls if c[0] == COLOR_DISABLED) assert normal[2] == _curses.COLOR_BLACK assert disabled[1:] == (_curses.COLOR_WHITE, _curses.COLOR_BLACK) def test_init_colors_survives_init_pair_rejecting_every_pair(): # Some serial-console terminfo entries claim start_color()/COLORS # support but reject every init_pair() call (e.g. COLOR_PAIRS too # small). init_colors() must not raise -- the app should still start, # just without custom colors. with patch.object(_curses, "start_color"), \ patch.object(_curses, "use_default_colors"), \ patch.object(_curses, "init_pair", side_effect=_curses.error("init_pair() returned ERR")), \ patch.object(_curses, "COLORS", 256, create=True): init_colors() # must not raise