"""Unit tests for CLI auth-failure classification.""" from __future__ import annotations import unittest from netx_api.ne_cli_errors import find_auth_failure_snippet, format_cli_failure class CliAuthClassifyTests(unittest.TestCase): def test_permission_denied_password(self): text = "banner\nca-oper@114.1.105.3: Permission denied (password).\n" self.assertIn("Permission denied", find_auth_failure_snippet(text) or "") def test_prompt_timeout_promoted_to_auth(self): exc = "ReadTimeout: Pattern not detected: '[>#]' in output." transcript = "Warning\nca-oper@114.1.105.3: Permission denied (password).\n" msg = format_cli_failure(exc, transcript) self.assertTrue(msg.startswith("auth_rejected:")) self.assertIn("Permission denied", msg) self.assertIn("prompt_timeout", msg) def test_plain_timeout_includes_session_log(self): exc = RuntimeError("ReadTimeout: Pattern not detected: '[>#]' in output.") msg = format_cli_failure(exc, "show running-config\n...still dumping...\n---- More ----\n") self.assertIn("ReadTimeout", msg) self.assertFalse(msg.startswith("auth_rejected:")) self.assertIn("session log", msg) self.assertIn("More", msg) def test_authentication_exception(self): class AuthenticationException(Exception): pass msg = format_cli_failure(AuthenticationException("target_auth_rejected: Permission denied")) self.assertTrue(msg.startswith("auth_rejected:")) def test_authentication_exception_empty_message(self): class AuthenticationException(Exception): pass msg = format_cli_failure(AuthenticationException()) self.assertTrue(msg.startswith("auth_rejected:")) def test_huawei_post_login_banner_not_auth_failure(self): """Bastion/SSH hop success banner must not be classified as auth reject.""" text = ( "Info: The max number of VTY users is 21, " "the number of current VTY users online is 1.\n" "The last successful login was performed at 19:28:16 08-02-2026 " "from 10.229.147.122 through SSH. Afterwards, 0 authentication " "failure occurred.\n" "" ) self.assertIsNone(find_auth_failure_snippet(text)) msg = format_cli_failure("ReadTimeout: Pattern not detected", text) self.assertFalse(msg.startswith("auth_rejected:")) # Still attach transcript for diagnostics (Huawei banner is not auth_rejected). self.assertIn("session log", msg) def test_real_auth_failure_still_detected_near_banner(self): text = ( "The last successful login was performed at 19:28:16 08-02-2026 " "from 10.229.147.122 through SSH. Afterwards, 0 authentication " "failure occurred.\n" "Error: Username or password is wrong.\n" ) self.assertIn("Username or password is wrong", find_auth_failure_snippet(text) or "") def test_zte_post_login_banner_not_auth_failure(self): """ZTE nested ssh hop: '0 authentication failure occurred' is login stats.""" text = ( "Welcome to ZXR10 ZXCTN 6120H Carrier-Class Router of ZTE Corporation\n" "Login at 09:43:53 08-31-2026 from 10.229.147.122 through SSH.\n" "The last successful login was performed at 09:43:44 08-31-2026 " "from 10.229.147.122 through SSH. Afterwards, 0 authentication " "failure occurred.\n" "AL5458-ACC-6120HS#" ) self.assertIsNone(find_auth_failure_snippet(text)) def test_zte_wrapped_afterwards_banner_not_auth_failure(self): """Narrow PTY wraps 'Afterwards' — must not classify as auth reject.""" text = ( "The last successful login was performed at 09:43:44 08-31-2026 " "from 10.229.147.122 through SSH. After\n" "wards, 0 authentication failure occurred.\n" "AL5458-ACC-6120HS#" ) self.assertIsNone(find_auth_failure_snippet(text)) def test_zte_severely_wrapped_afterwards_banner_not_auth_failure(self): """Production saw 'rwards' after mid-word wrap — still login stats, not reject.""" text = ( "from 10.229.147.122 through SSH. Afterwa\n" "rwards, 0 authentication failure occurred.\n" "AL5458-ACC-6120HS#" ) self.assertIsNone(find_auth_failure_snippet(text)) if __name__ == "__main__": unittest.main()