test_setup_feishu.py
1 """Tests for _setup_feishu() in hermes_cli/gateway.py. 2 3 Verifies that the interactive setup writes env vars that correctly drive the 4 Feishu adapter: credentials, connection mode, DM policy, and group policy. 5 """ 6 7 import os 8 from unittest.mock import patch 9 10 11 # --------------------------------------------------------------------------- 12 # Helpers 13 # --------------------------------------------------------------------------- 14 15 def _run_setup_feishu( 16 *, 17 qr_result=None, 18 prompt_yes_no_responses=None, 19 prompt_choice_responses=None, 20 prompt_responses=None, 21 existing_env=None, 22 ): 23 """Run _setup_feishu() with mocked I/O and return the env vars that were saved. 24 25 Returns a dict of {env_var_name: value} for all save_env_value calls. 26 """ 27 existing_env = existing_env or {} 28 prompt_yes_no_responses = list(prompt_yes_no_responses or [True]) 29 # QR path: method(0), dm(0), group(0) — 3 choices (no connection mode) 30 # Manual path: method(1), domain(0), connection(0), dm(0), group(0) — 5 choices 31 prompt_choice_responses = list(prompt_choice_responses or [0, 0, 0]) 32 prompt_responses = list(prompt_responses or [""]) 33 34 saved_env = {} 35 36 def mock_save(name, value): 37 saved_env[name] = value 38 39 def mock_get(name): 40 return existing_env.get(name, "") 41 42 with patch("hermes_cli.gateway.save_env_value", side_effect=mock_save), \ 43 patch("hermes_cli.gateway.get_env_value", side_effect=mock_get), \ 44 patch("hermes_cli.gateway.prompt_yes_no", side_effect=prompt_yes_no_responses), \ 45 patch("hermes_cli.gateway.prompt_choice", side_effect=prompt_choice_responses), \ 46 patch("hermes_cli.gateway.prompt", side_effect=prompt_responses), \ 47 patch("hermes_cli.gateway.print_info"), \ 48 patch("hermes_cli.gateway.print_success"), \ 49 patch("hermes_cli.gateway.print_warning"), \ 50 patch("hermes_cli.gateway.print_error"), \ 51 patch("hermes_cli.gateway.color", side_effect=lambda t, c: t), \ 52 patch("gateway.platforms.feishu.qr_register", return_value=qr_result): 53 54 from hermes_cli.gateway import _setup_feishu 55 _setup_feishu() 56 57 return saved_env 58 59 60 # --------------------------------------------------------------------------- 61 # QR scan-to-create path 62 # --------------------------------------------------------------------------- 63 64 class TestSetupFeishuQrPath: 65 """Tests for the QR scan-to-create happy path.""" 66 67 def test_qr_success_saves_core_credentials(self): 68 env = _run_setup_feishu( 69 qr_result={ 70 "app_id": "cli_test", 71 "app_secret": "secret_test", 72 "domain": "feishu", 73 "open_id": "ou_owner", 74 "bot_name": "TestBot", 75 "bot_open_id": "ou_bot", 76 }, 77 prompt_yes_no_responses=[True], # Start QR 78 prompt_choice_responses=[0, 0, 0], # method=QR, dm=pairing, group=open 79 prompt_responses=[""], # home channel: skip 80 ) 81 assert env["FEISHU_APP_ID"] == "cli_test" 82 assert env["FEISHU_APP_SECRET"] == "secret_test" 83 assert env["FEISHU_DOMAIN"] == "feishu" 84 85 def test_qr_success_does_not_persist_bot_identity(self): 86 """Bot identity is discovered at runtime by _hydrate_bot_identity — not persisted 87 in env, so it stays fresh if the user renames the bot later.""" 88 env = _run_setup_feishu( 89 qr_result={ 90 "app_id": "cli_test", 91 "app_secret": "secret_test", 92 "domain": "feishu", 93 "open_id": "ou_owner", 94 "bot_name": "TestBot", 95 "bot_open_id": "ou_bot", 96 }, 97 prompt_yes_no_responses=[True], 98 prompt_choice_responses=[0, 0, 0], 99 prompt_responses=[""], 100 ) 101 assert "FEISHU_BOT_OPEN_ID" not in env 102 assert "FEISHU_BOT_NAME" not in env 103 104 105 # --------------------------------------------------------------------------- 106 # Connection mode 107 # --------------------------------------------------------------------------- 108 109 class TestSetupFeishuConnectionMode: 110 """Connection mode: QR always websocket, manual path lets user choose.""" 111 112 def test_qr_path_defaults_to_websocket(self): 113 env = _run_setup_feishu( 114 qr_result={ 115 "app_id": "cli_test", "app_secret": "s", "domain": "feishu", 116 "open_id": None, "bot_name": None, "bot_open_id": None, 117 }, 118 prompt_choice_responses=[0, 0, 0], # method=QR, dm=pairing, group=open 119 prompt_responses=[""], 120 ) 121 assert env["FEISHU_CONNECTION_MODE"] == "websocket" 122 123 @patch("gateway.platforms.feishu.probe_bot", return_value=None) 124 def test_manual_path_websocket(self, _mock_probe): 125 env = _run_setup_feishu( 126 qr_result=None, 127 prompt_choice_responses=[1, 0, 0, 0, 0], # method=manual, domain=feishu, connection=ws, dm=pairing, group=open 128 prompt_responses=["cli_manual", "secret_manual", ""], # app_id, app_secret, home_channel 129 ) 130 assert env["FEISHU_CONNECTION_MODE"] == "websocket" 131 132 @patch("gateway.platforms.feishu.probe_bot", return_value=None) 133 def test_manual_path_webhook(self, _mock_probe): 134 env = _run_setup_feishu( 135 qr_result=None, 136 prompt_choice_responses=[1, 0, 1, 0, 0], # method=manual, domain=feishu, connection=webhook, dm=pairing, group=open 137 prompt_responses=["cli_manual", "secret_manual", ""], # app_id, app_secret, home_channel 138 ) 139 assert env["FEISHU_CONNECTION_MODE"] == "webhook" 140 141 142 # --------------------------------------------------------------------------- 143 # DM security policy 144 # --------------------------------------------------------------------------- 145 146 class TestSetupFeishuDmPolicy: 147 """DM policy must use platform-scoped FEISHU_ALLOW_ALL_USERS, not the global flag.""" 148 149 def _run_with_dm_choice(self, dm_choice_idx, prompt_responses=None): 150 return _run_setup_feishu( 151 qr_result={ 152 "app_id": "cli_test", "app_secret": "s", "domain": "feishu", 153 "open_id": "ou_owner", "bot_name": None, "bot_open_id": None, 154 }, 155 prompt_yes_no_responses=[True], 156 prompt_choice_responses=[0, dm_choice_idx, 0], # method=QR, dm=<choice>, group=open 157 prompt_responses=prompt_responses or [""], 158 ) 159 160 def test_pairing_sets_feishu_allow_all_false(self): 161 env = self._run_with_dm_choice(0) 162 assert env["FEISHU_ALLOW_ALL_USERS"] == "false" 163 assert env["FEISHU_ALLOWED_USERS"] == "" 164 assert "GATEWAY_ALLOW_ALL_USERS" not in env 165 166 def test_allow_all_sets_feishu_allow_all_true(self): 167 env = self._run_with_dm_choice(1) 168 assert env["FEISHU_ALLOW_ALL_USERS"] == "true" 169 assert env["FEISHU_ALLOWED_USERS"] == "" 170 assert "GATEWAY_ALLOW_ALL_USERS" not in env 171 172 def test_allowlist_sets_feishu_allow_all_false_with_list(self): 173 env = self._run_with_dm_choice(2, prompt_responses=["ou_user1,ou_user2", ""]) 174 assert env["FEISHU_ALLOW_ALL_USERS"] == "false" 175 assert env["FEISHU_ALLOWED_USERS"] == "ou_user1,ou_user2" 176 assert "GATEWAY_ALLOW_ALL_USERS" not in env 177 178 def test_allowlist_prepopulates_with_scan_owner_open_id(self): 179 """When open_id is available from QR scan, it should be the default allowlist value.""" 180 # We return the owner's open_id from prompt (+ empty home channel). 181 env = self._run_with_dm_choice(2, prompt_responses=["ou_owner", ""]) 182 assert env["FEISHU_ALLOWED_USERS"] == "ou_owner" 183 184 185 186 # --------------------------------------------------------------------------- 187 # Group policy 188 # --------------------------------------------------------------------------- 189 190 class TestSetupFeishuGroupPolicy: 191 192 def test_open_with_mention(self): 193 env = _run_setup_feishu( 194 qr_result={ 195 "app_id": "cli_test", "app_secret": "s", "domain": "feishu", 196 "open_id": None, "bot_name": None, "bot_open_id": None, 197 }, 198 prompt_yes_no_responses=[True], 199 prompt_choice_responses=[0, 0, 0], # method=QR, dm=pairing, group=open 200 prompt_responses=[""], 201 ) 202 assert env["FEISHU_GROUP_POLICY"] == "open" 203 204 def test_disabled(self): 205 env = _run_setup_feishu( 206 qr_result={ 207 "app_id": "cli_test", "app_secret": "s", "domain": "feishu", 208 "open_id": None, "bot_name": None, "bot_open_id": None, 209 }, 210 prompt_yes_no_responses=[True], 211 prompt_choice_responses=[0, 0, 1], # method=QR, dm=pairing, group=disabled 212 prompt_responses=[""], 213 ) 214 assert env["FEISHU_GROUP_POLICY"] == "disabled" 215 216 217 # --------------------------------------------------------------------------- 218 # Adapter integration: env vars → FeishuAdapterSettings 219 # --------------------------------------------------------------------------- 220 221 class TestSetupFeishuAdapterIntegration: 222 """Verify that env vars written by _setup_feishu() produce a valid adapter config. 223 224 This bridges the gap between 'setup wrote the right env vars' and 225 'the adapter will actually initialize correctly from those vars'. 226 """ 227 228 def _make_env_from_setup(self, dm_idx=0, group_idx=0): 229 """Run _setup_feishu via QR path and return the env vars it would write.""" 230 return _run_setup_feishu( 231 qr_result={ 232 "app_id": "cli_test_app", 233 "app_secret": "test_secret_value", 234 "domain": "feishu", 235 "open_id": "ou_owner", 236 "bot_name": "IntegrationBot", 237 "bot_open_id": "ou_bot_integration", 238 }, 239 prompt_yes_no_responses=[True], 240 prompt_choice_responses=[0, dm_idx, group_idx], # method=QR, dm, group 241 prompt_responses=[""], 242 ) 243 244 @patch.dict(os.environ, {}, clear=True) 245 def test_qr_env_produces_valid_adapter_settings(self): 246 """QR setup → adapter initializes with websocket mode.""" 247 env = self._make_env_from_setup() 248 249 with patch.dict(os.environ, env, clear=True): 250 from gateway.config import PlatformConfig 251 from gateway.platforms.feishu import FeishuAdapter 252 adapter = FeishuAdapter(PlatformConfig()) 253 assert adapter._app_id == "cli_test_app" 254 assert adapter._app_secret == "test_secret_value" 255 assert adapter._domain_name == "feishu" 256 assert adapter._connection_mode == "websocket" 257 258 @patch.dict(os.environ, {}, clear=True) 259 def test_open_dm_env_sets_correct_adapter_state(self): 260 """Setup with 'allow all DMs' → adapter sees allow-all flag.""" 261 env = self._make_env_from_setup(dm_idx=1) 262 263 with patch.dict(os.environ, env, clear=True): 264 from gateway.platforms.feishu import FeishuAdapter 265 from gateway.config import PlatformConfig 266 # Verify adapter initializes without error and env var is correct. 267 FeishuAdapter(PlatformConfig()) 268 assert os.getenv("FEISHU_ALLOW_ALL_USERS") == "true" 269 270 @patch.dict(os.environ, {}, clear=True) 271 def test_group_open_env_sets_adapter_group_policy(self): 272 """Setup with 'open groups' → adapter group_policy is 'open'.""" 273 env = self._make_env_from_setup(group_idx=0) 274 275 with patch.dict(os.environ, env, clear=True): 276 from gateway.config import PlatformConfig 277 from gateway.platforms.feishu import FeishuAdapter 278 adapter = FeishuAdapter(PlatformConfig()) 279 assert adapter._group_policy == "open"