"""Tests for matrix_hermes.cli module.""" import json from unittest.mock import MagicMock, patch import pytest from matrix_hermes.cli import ( _mask_token, build_parser, cmd_check, cmd_login, cmd_logout, cmd_show_config, cmd_whoami, main, ) class TestMaskToken: """Tests for _mask_token helper.""" def test_short_token(self): assert _mask_token("abc") == "***" def test_long_token(self): result = _mask_token("syt_abcdefghijklmnopqrstuvwxyz") assert result.startswith("syt_ab") assert result.endswith("uvwxyz") assert "..." in result def test_exact_length(self): """12-char token with visible=6: len<=12 means fully masked.""" result = _mask_token("123456789012") assert result == "************" def test_just_over_threshold(self): """13-char token with visible=6: len>12 means partial mask.""" result = _mask_token("1234567890123") assert result == "123456...890123" class TestParser: """Tests for CLI argument parser.""" def test_build_parser(self): parser = build_parser() assert "login" in parser._subparsers._group_actions[0].choices assert "whoami" in parser._subparsers._group_actions[0].choices assert "check" in parser._subparsers._group_actions[0].choices assert "show-config" in parser._subparsers._group_actions[0].choices def test_no_args_shows_help(self, capsys): """Running with no args shows help.""" result = main([]) # Should not crash captured = capsys.readouterr() assert "usage" in captured.out.lower() or result == 0 class TestCmdCheck: """Tests for 'check' command.""" def test_not_configured(self, clean_env, capsys): """When no Matrix vars are set, check should fail.""" args = build_parser().parse_args(["check"]) result = cmd_check(args) assert result == 1 captured = capsys.readouterr() assert "not configured" in captured.out.lower() def test_configured(self, mock_env, capsys): """When Matrix vars are set, check should succeed.""" args = build_parser().parse_args(["check"]) result = cmd_check(args) assert result == 0 captured = capsys.readouterr() assert "valid" in captured.out.lower() class TestCmdShowConfig: """Tests for 'show-config' command.""" def test_output_format(self, mock_env, capsys): """show-config outputs env var format.""" args = build_parser().parse_args(["show-config"]) result = cmd_show_config(args) assert result == 0 captured = capsys.readouterr() assert "MATRIX_HOMESERVER=" in captured.out assert "MATRIX_ACCESS_TOKEN=" in captured.out def test_redacted(self, mock_env, capsys): """show-config --redact masks sensitive values.""" args = build_parser().parse_args(["show-config", "--redact"]) result = cmd_show_config(args) assert result == 0 captured = capsys.readouterr() # Token should be masked assert "syt_test_token" not in captured.out assert "syt_test" in captured.out or "..." in captured.out class TestCmdLogin: """Tests for 'login' command.""" def test_no_homeserver(self, clean_env, capsys): """Login fails without homeserver.""" args = build_parser().parse_args(["login"]) result = cmd_login(args) assert result == 1 def test_with_valid_token(self, mock_env, capsys): """Login with valid token prints success.""" with patch("urllib.request.urlopen") as mock_urlopen: mock_resp = MagicMock() mock_resp.read.return_value = json.dumps( {"user_id": "@bot:example.org", "device_id": "DEV1"} ).encode() mock_resp.__enter__.return_value = mock_resp mock_urlopen.return_value = mock_resp args = build_parser().parse_args(["login"]) result = cmd_login(args) assert result == 0 def test_with_bad_credentials(self, mock_env, capsys): """Login with bad credentials prints error.""" with patch("urllib.request.urlopen") as mock_urlopen: mock_resp = MagicMock() mock_resp.read.return_value = json.dumps( {"errcode": "M_FORBIDDEN", "error": "Bad password"} ).encode() mock_resp.__enter__.return_value = mock_resp mock_urlopen.return_value = mock_resp # Override with password-based login (no valid token) import os os.environ["MATRIX_ACCESS_TOKEN"] = "" os.environ["MATRIX_PASSWORD"] = "wrong" args = build_parser().parse_args(["login"]) result = cmd_login(args) assert result == 1 class TestCmdWhoami: """Tests for 'whoami' command.""" def test_no_homeserver(self, clean_env, capsys): """whoami fails without homeserver.""" args = build_parser().parse_args(["whoami"]) result = cmd_whoami(args) assert result == 1 def test_valid(self, mock_env, capsys): """whoami with valid token.""" with patch("urllib.request.urlopen") as mock_urlopen: mock_resp = MagicMock() mock_resp.read.return_value = json.dumps( {"user_id": "@bot:example.org"} ).encode() mock_resp.__enter__.return_value = mock_resp mock_urlopen.return_value = mock_resp args = build_parser().parse_args(["whoami"]) result = cmd_whoami(args) assert result == 0 def test_invalid(self, mock_env, capsys): """whoami with invalid token.""" with patch("urllib.request.urlopen") as mock_urlopen: mock_resp = MagicMock() mock_resp.read.return_value = json.dumps( {"errcode": "M_UNKNOWN_TOKEN", "error": "Unrecognised"} ).encode() mock_resp.__enter__.return_value = mock_resp mock_urlopen.return_value = mock_resp args = build_parser().parse_args(["whoami"]) result = cmd_whoami(args) assert result == 1 class TestCmdLogout: """Tests for 'logout' command.""" def test_no_homeserver(self, clean_env, capsys): """logout fails without homeserver.""" args = build_parser().parse_args(["logout"]) result = cmd_logout(args) assert result == 1 def test_success(self, mock_env, capsys): """logout with valid token.""" with patch("urllib.request.urlopen") as mock_urlopen: mock_resp = MagicMock() mock_resp.read.return_value = json.dumps({}).encode() mock_resp.__enter__.return_value = mock_resp mock_urlopen.return_value = mock_resp args = build_parser().parse_args(["logout"]) result = cmd_logout(args) assert result == 0 # --------------------------------------------------------------------------- # New async command parser tests # --------------------------------------------------------------------------- class TestNewCommandsParser: """Tests for parser entries of rooms, send, sync commands.""" def test_rooms_subparser_exists(self): parser = build_parser() choices = parser._subparsers._group_actions[0].choices assert "rooms" in choices assert "send" in choices assert "sync" in choices def test_rooms_list(self): args = build_parser().parse_args(["rooms", "list"]) assert args.command == "rooms" assert args.rooms_action == "list" def test_rooms_create(self): args = build_parser().parse_args( ["rooms", "create", "--name", "Test", "--topic", "Hello", "--public"] ) assert args.rooms_action == "create" assert args.name == "Test" assert args.topic == "Hello" assert args.public is True def test_rooms_join(self): args = build_parser().parse_args(["rooms", "join", "!room:example.org"]) assert args.rooms_action == "join" assert args.room == "!room:example.org" def test_rooms_leave(self): args = build_parser().parse_args(["rooms", "leave", "!room:example.org"]) assert args.rooms_action == "leave" assert args.room == "!room:example.org" def test_rooms_info(self): args = build_parser().parse_args(["rooms", "info", "!room:example.org"]) assert args.rooms_action == "info" assert args.room == "!room:example.org" def test_rooms_members(self): args = build_parser().parse_args(["rooms", "members", "!room:example.org"]) assert args.rooms_action == "members" def test_rooms_invite(self): args = build_parser().parse_args( ["rooms", "invite", "!room:example.org", "@alice:example.org"] ) assert args.rooms_action == "invite" assert args.user == "@alice:example.org" def test_send_text(self): args = build_parser().parse_args(["send", "text", "!room:example.org", "Hello world"]) assert args.send_action == "text" assert args.room == "!room:example.org" assert args.body == "Hello world" def test_send_text_with_reply(self): args = build_parser().parse_args( ["send", "text", "!room:example.org", "Replying", "--reply-to", "$evt:example.org"] ) assert args.reply_to == "$evt:example.org" def test_send_image(self): args = build_parser().parse_args( ["send", "image", "!room:example.org", "/tmp/photo.jpg", "--caption", "My photo"] ) assert args.send_action == "image" assert args.path == "/tmp/photo.jpg" assert args.caption == "My photo" def test_send_file(self): args = build_parser().parse_args( ["send", "file", "!room:example.org", "/tmp/doc.pdf"] ) assert args.send_action == "file" def test_sync_listen(self): args = build_parser().parse_args(["sync", "listen"]) assert args.sync_action == "listen" assert args.filter_type == [] assert args.room == [] assert args.show_all is False def test_sync_listen_with_filters(self): args = build_parser().parse_args( ["sync", "listen", "--filter-type", "m.room.message", "--room", "!test:example.org", "--show-all"] ) assert args.filter_type == ["m.room.message"] assert args.room == ["!test:example.org"] assert args.show_all is True