mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Fix Huawei WebCRT hop login echo doubling and safer Change-now handling.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
cacd8c7b75
commit
0a550abcb0
9 changed files with 498 additions and 77 deletions
|
|
@ -280,7 +280,9 @@ class BastionConnectImplTests(unittest.TestCase):
|
|||
password="bastion-pass",
|
||||
timeout=unittest.mock.ANY,
|
||||
)
|
||||
interact.assert_called_once_with(conn, "target-user", "target-pass", progress_cb=None)
|
||||
interact.assert_called_once_with(
|
||||
conn, "target-user", "target-pass", progress_cb=None, emit_raw=True
|
||||
)
|
||||
|
||||
|
||||
class BastionInteractiveHandlerTests(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -131,6 +131,30 @@ class HuaweiStelnetAuthTests(unittest.TestCase):
|
|||
sent = [c.args[1] for c in mock_send.call_args_list]
|
||||
self.assertEqual(sent, ["ipran", "Y", "N", "secret"])
|
||||
|
||||
@patch("netx_api.ne_session_connect._read_channel")
|
||||
@patch("netx_api.ne_session_connect._send_line")
|
||||
def test_answers_password_change_after_login(
|
||||
self,
|
||||
mock_send: MagicMock,
|
||||
mock_read: MagicMock,
|
||||
) -> None:
|
||||
"""Huawei ``Change now? [Y/N]:`` after password must not hang auth until timeout."""
|
||||
chunks = [
|
||||
"Please input the username:",
|
||||
"Enter password:",
|
||||
"Change now? [Y/N]:",
|
||||
"<HW-TARGET>\n",
|
||||
]
|
||||
mock_read.side_effect = lambda *_a, **_k: chunks.pop(0) if chunks else ""
|
||||
conn = MagicMock()
|
||||
seen: list[str] = []
|
||||
_interactive_target_auth(conn, "admin", "secret", progress_cb=seen.append)
|
||||
self.assertEqual(
|
||||
[c.args[1] for c in mock_send.call_args_list],
|
||||
["admin", "secret", "N"],
|
||||
)
|
||||
self.assertTrue(any("password-change" in p for p in seen))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -108,9 +108,27 @@ class WebcrtServiceTests(unittest.TestCase):
|
|||
self.assertTrue(svc._looks_like_cli_prompt("<r1>"))
|
||||
# Stray ':' after Huawei prompt must still count as prompted (no extra Enter).
|
||||
self.assertTrue(svc._looks_like_cli_prompt("<r1>:"))
|
||||
self.assertTrue(svc._looks_like_cli_prompt("<r1>N"))
|
||||
# Trailing [netx] progress lines must not hide an already-visible CLI prompt.
|
||||
self.assertTrue(svc._looks_like_cli_prompt("<HW>\r\n[netx] password-change → N\r\n"))
|
||||
self.assertEqual(svc.prepare_bootstrap_output("banner\n<r1>:"), "banner\n<r1>")
|
||||
self.assertTrue(svc._looks_like_password_change_prompt("Change now? [Y/N]:"))
|
||||
self.assertFalse(svc._looks_like_password_change_prompt("Change now? [Y/N]:N"))
|
||||
# Still pending only while sitting on Change-now with no answer/prompt after.
|
||||
self.assertTrue(svc._password_change_still_pending("Change now? [Y/N]:"))
|
||||
# Netmiko already sent N — do not send a second N (would become <r1>N).
|
||||
self.assertFalse(
|
||||
svc._password_change_still_pending(
|
||||
"Change now? [Y/N]:\nN\nInfo: VTY\n<r1>"
|
||||
)
|
||||
)
|
||||
self.assertFalse(svc._password_change_still_pending("Change now? [Y/N]:\nN"))
|
||||
self.assertFalse(
|
||||
svc._password_change_still_pending(
|
||||
"The password needs to be changed. Change now? [Y/N]:\n"
|
||||
"Info: The max number of VTY users is 5\n<r1>"
|
||||
)
|
||||
)
|
||||
# Bare / stelnet host-key [Y/N] must not be treated as password-change.
|
||||
self.assertFalse(svc._looks_like_password_change_prompt("[Y/N]:"))
|
||||
self.assertFalse(
|
||||
|
|
@ -173,9 +191,10 @@ class WebcrtServiceTests(unittest.TestCase):
|
|||
)
|
||||
sess = svc.get_session(out["session_id"])
|
||||
assert sess is not None
|
||||
boot = sess.bootstrap_output.decode("utf-8", errors="replace")
|
||||
self.assertIn("****", boot)
|
||||
self.assertIn("R2#", boot)
|
||||
# Live connect-echo owns the login transcript (bootstrap stays empty to avoid double play).
|
||||
echo = sess.connect_echo_text()
|
||||
self.assertIn("****", echo)
|
||||
self.assertIn("R2#", echo)
|
||||
svc.close_session(out["session_id"], reason="test")
|
||||
|
||||
@patch.object(reg, "_audit")
|
||||
|
|
@ -318,9 +337,9 @@ class WebcrtServiceTests(unittest.TestCase):
|
|||
self.assertFalse(out.get("cli_hop")) # bastion hop is not vendor CLI hop guard
|
||||
sess = svc.get_session(out["session_id"])
|
||||
assert sess is not None
|
||||
boot = sess.bootstrap_output.decode("utf-8", errors="replace")
|
||||
self.assertIn("Username:huawei", boot)
|
||||
self.assertIn("<r1>", boot)
|
||||
echo = sess.connect_echo_text()
|
||||
self.assertIn("Username:huawei", echo)
|
||||
self.assertIn("<r1>", echo)
|
||||
self.assertFalse(sess.needs_live_prompt)
|
||||
before = list(fake.written)
|
||||
sess.write_stdin("\n")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue