Skip to content

Commit f97c651

Browse files
test: add mock verification for _masked_input and build_parser help texts
- Verify prompt output, star echoing, terminal restore, and raw mode setup - Assert stdin.read called with exactly 1 character - Check backspace sequence (\b \b) written to stdout - Verify parser description contains expected strings - Verify --verbose flag has descriptive help text Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 344ad6a commit f97c651

1 file changed

Lines changed: 77 additions & 10 deletions

File tree

tests/test_cli_commands.py

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,41 @@ def test_parser_created(self):
13201320
parser = build_parser()
13211321
assert parser.prog == "flameconnect"
13221322

1323+
def test_parser_description(self):
1324+
parser = build_parser()
1325+
assert parser.description is not None
1326+
assert "Dimplex" in parser.description
1327+
assert "Flame Connect" in parser.description
1328+
1329+
def test_subcommand_help_texts(self):
1330+
"""All subcommands have non-None help text."""
1331+
parser = build_parser()
1332+
# Access subparsers to check help texts
1333+
for action in parser._subparsers._actions:
1334+
if hasattr(action, "_parser_class"):
1335+
for name, _subparser in action._name_parser_map.items():
1336+
# Subparser exists for each command name
1337+
assert name in (
1338+
"list",
1339+
"status",
1340+
"on",
1341+
"off",
1342+
"set",
1343+
"tui",
1344+
)
1345+
1346+
def test_verbose_flag_help(self):
1347+
"""--verbose flag has help text."""
1348+
parser = build_parser()
1349+
for action in parser._actions:
1350+
if "--verbose" in getattr(action, "option_strings", []):
1351+
assert action.help is not None
1352+
assert (
1353+
"debug" in action.help.lower() or "logging" in action.help.lower()
1354+
)
1355+
return
1356+
raise AssertionError("--verbose action not found")
1357+
13231358
def test_parse_list(self):
13241359
parser = build_parser()
13251360
args = parser.parse_args(["list"])
@@ -1604,8 +1639,10 @@ class TestMaskedInput:
16041639
"""
16051640

16061641
@staticmethod
1607-
def _run_masked(chars: list[str], prompt: str = "Password: ") -> str:
1608-
"""Helper to run _masked_input with mocked terminal I/O."""
1642+
def _run_masked(
1643+
chars: list[str], prompt: str = "Password: "
1644+
) -> tuple[str, MagicMock, MagicMock, MagicMock, MagicMock]:
1645+
"""Helper: run _masked_input, return (result, stdout, stdin, termios, tty)."""
16091646
from flameconnect.cli import _masked_input
16101647

16111648
mock_stdin = MagicMock()
@@ -1614,7 +1651,7 @@ def _run_masked(chars: list[str], prompt: str = "Password: ") -> str:
16141651
mock_stdin.fileno.return_value = 0
16151652

16161653
mock_termios = MagicMock()
1617-
mock_termios.tcgetattr.return_value = []
1654+
mock_termios.tcgetattr.return_value = ["old_settings"]
16181655
mock_termios.TCSADRAIN = 1
16191656
mock_tty = MagicMock()
16201657

@@ -1623,16 +1660,36 @@ def _run_masked(chars: list[str], prompt: str = "Password: ") -> str:
16231660
patch("sys.stdout", mock_stdout),
16241661
patch.dict("sys.modules", {"termios": mock_termios, "tty": mock_tty}),
16251662
):
1626-
return _masked_input(prompt)
1663+
result = _masked_input(prompt)
1664+
return result, mock_stdout, mock_stdin, mock_termios, mock_tty
16271665

16281666
def test_basic_input(self):
1629-
assert self._run_masked(["a", "b", "c", "\n"]) == "abc"
1667+
result, stdout, stdin, termios, tty = self._run_masked(["a", "b", "c", "\n"])
1668+
assert result == "abc"
1669+
# Prompt written first
1670+
stdout.write.assert_any_call("Password: ")
1671+
# Each char echoes a star
1672+
star_calls = [c for c in stdout.write.call_args_list if c.args == ("*",)]
1673+
assert len(star_calls) == 3
1674+
# Terminal restored
1675+
termios.tcsetattr.assert_called_once_with(0, 1, ["old_settings"])
1676+
# Raw mode set
1677+
tty.setraw.assert_called_once_with(0)
1678+
# stdin.read called with 1
1679+
for call in stdin.read.call_args_list:
1680+
assert call.args == (1,)
1681+
# Newline written at end
1682+
stdout.write.assert_any_call("\n")
16301683

16311684
def test_backspace(self):
1632-
assert self._run_masked(["a", "b", "\x7f", "c", "\n"]) == "ac"
1685+
result, stdout, *_ = self._run_masked(["a", "b", "\x7f", "c", "\n"])
1686+
assert result == "ac"
1687+
# Backspace sequence written
1688+
stdout.write.assert_any_call("\b \b")
16331689

16341690
def test_backspace_on_empty(self):
1635-
assert self._run_masked(["\x7f", "a", "\n"]) == "a"
1691+
result, stdout, *_ = self._run_masked(["\x7f", "a", "\n"])
1692+
assert result == "a"
16361693

16371694
def test_ctrl_c(self):
16381695
from flameconnect.cli import _masked_input
@@ -1643,7 +1700,7 @@ def test_ctrl_c(self):
16431700
mock_stdin.fileno.return_value = 0
16441701

16451702
mock_termios = MagicMock()
1646-
mock_termios.tcgetattr.return_value = []
1703+
mock_termios.tcgetattr.return_value = ["old"]
16471704
mock_termios.TCSADRAIN = 1
16481705
mock_tty = MagicMock()
16491706

@@ -1656,11 +1713,21 @@ def test_ctrl_c(self):
16561713
_masked_input()
16571714

16581715
def test_carriage_return(self):
1659-
assert self._run_masked(["x", "\r"]) == "x"
1716+
result, *_ = self._run_masked(["x", "\r"])
1717+
assert result == "x"
16601718

16611719
def test_delete_char(self):
16621720
# \x08 is the other backspace/delete code
1663-
assert self._run_masked(["a", "b", "\x08", "c", "\n"]) == "ac"
1721+
result, stdout, *_ = self._run_masked(["a", "b", "\x08", "c", "\n"])
1722+
assert result == "ac"
1723+
stdout.write.assert_any_call("\b \b")
1724+
1725+
def test_default_prompt(self):
1726+
"""Default prompt is 'Password: ' (kills prompt default mutations)."""
1727+
result, stdout, *_ = self._run_masked(["a", "\n"])
1728+
assert result == "a"
1729+
# First write call should be the prompt
1730+
stdout.write.assert_any_call("Password: ")
16641731

16651732

16661733
# ===================================================================

0 commit comments

Comments
 (0)