cli-anything-cliproxyapi 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,621 @@
1
+ """单元测试 - 使用合成数据和 mock,不依赖外部服务。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ import tempfile
9
+ from unittest.mock import MagicMock, patch, PropertyMock
10
+
11
+ import pytest
12
+
13
+ # 确保包可导入
14
+ HARNESS_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
15
+ sys.path.insert(0, HARNESS_ROOT)
16
+
17
+ from cli_anything.cliproxyapi.core.client import ConnectionConfig, ManagementClient
18
+ from cli_anything.cliproxyapi.core.config import ConfigManager
19
+ from cli_anything.cliproxyapi.core.auth import AuthManager
20
+ from cli_anything.cliproxyapi.core.oauth import OAuthManager
21
+ from cli_anything.cliproxyapi.core.models import ModelManager
22
+ from cli_anything.cliproxyapi.core.usage import UsageManager
23
+ from cli_anything.cliproxyapi.core.logs import LogManager
24
+ from cli_anything.cliproxyapi.core.api_keys import APIKeyManager
25
+ from cli_anything.cliproxyapi.core.proxy import ProxyManager
26
+
27
+
28
+ # ============================================================
29
+ # 辅助工具
30
+ # ============================================================
31
+
32
+ def _mock_response(json_data=None, text_data=None, status_code=200):
33
+ """创建 mock HTTP 响应。"""
34
+ resp = MagicMock()
35
+ resp.status_code = status_code
36
+ resp.json.return_value = json_data or {}
37
+ resp.text = text_data or json.dumps(json_data or {})
38
+ resp.raise_for_status.return_value = None
39
+ return resp
40
+
41
+
42
+ def _make_client(url="http://localhost:8317", key="test-key"):
43
+ conn = ConnectionConfig(url=url, key=key)
44
+ return ManagementClient(conn)
45
+
46
+
47
+ # ============================================================
48
+ # ConnectionConfig 测试
49
+ # ============================================================
50
+
51
+ class TestConnectionConfig:
52
+
53
+ def test_explicit_params(self):
54
+ conn = ConnectionConfig(url="http://example.com:8317", key="mykey")
55
+ assert conn.base_url == "http://example.com:8317"
56
+ assert conn.management_key == "mykey"
57
+ assert conn.is_configured
58
+
59
+ def test_env_vars(self):
60
+ with patch.dict(os.environ, {"CPA_URL": "http://env:8317", "CPA_KEY": "envkey"}):
61
+ conn = ConnectionConfig()
62
+ assert conn.base_url == "http://env:8317"
63
+ assert conn.management_key == "envkey"
64
+
65
+ def test_not_configured(self):
66
+ with patch.dict(os.environ, {}, clear=True):
67
+ conn = ConnectionConfig()
68
+ assert not conn.is_configured
69
+
70
+ def test_trailing_slash_stripped(self):
71
+ conn = ConnectionConfig(url="http://example.com:8317/", key="k")
72
+ assert conn.base_url == "http://example.com:8317"
73
+
74
+ def test_save_and_load(self):
75
+ with tempfile.TemporaryDirectory() as tmpdir:
76
+ config_path = os.path.join(tmpdir, ".cliproxyapi-cli.yaml")
77
+ with patch.object(ConnectionConfig, "_load_from_config") as mock_load:
78
+ # 直接测试 save 逻辑
79
+ import yaml
80
+ data = {"url": "http://saved:8317", "key": "savedkey"}
81
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
82
+ yaml.dump(data, f)
83
+ path = f.name
84
+ assert os.path.exists(path)
85
+ os.unlink(path)
86
+
87
+
88
+ # ============================================================
89
+ # ManagementClient 测试
90
+ # ============================================================
91
+
92
+ class TestManagementClient:
93
+
94
+ def test_url_construction(self):
95
+ client = _make_client()
96
+ assert client._url("/config") == "http://localhost:8317/v0/management/config"
97
+
98
+ def test_headers(self):
99
+ client = _make_client(key="test-key")
100
+ headers = client._headers
101
+ assert headers["Authorization"] == "Bearer test-key"
102
+ assert headers["Content-Type"] == "application/json"
103
+
104
+ @patch("cli_anything.cliproxyapi.core.client.requests.Session")
105
+ def test_get(self, mock_session_cls):
106
+ mock_session = MagicMock()
107
+ mock_session.get.return_value = _mock_response({"status": "ok"})
108
+ mock_session_cls.return_value = mock_session
109
+ client = _make_client()
110
+ resp = client.get("/config")
111
+ assert resp.json() == {"status": "ok"}
112
+
113
+ @patch("cli_anything.cliproxyapi.core.client.requests.Session")
114
+ def test_post(self, mock_session_cls):
115
+ mock_session = MagicMock()
116
+ mock_session.post.return_value = _mock_response({"status": "ok"})
117
+ mock_session_cls.return_value = mock_session
118
+ client = _make_client()
119
+ resp = client.post("/api-keys", json_data={"value": "newkey"})
120
+ assert resp.json() == {"status": "ok"}
121
+
122
+ @patch("cli_anything.cliproxyapi.core.client.requests.Session")
123
+ def test_delete(self, mock_session_cls):
124
+ mock_session = MagicMock()
125
+ mock_session.delete.return_value = _mock_response({"status": "ok"})
126
+ mock_session_cls.return_value = mock_session
127
+ client = _make_client()
128
+ resp = client.delete("/api-keys", json_data={"value": "key1"})
129
+ assert resp.json() == {"status": "ok"}
130
+
131
+ @patch("cli_anything.cliproxyapi.core.client.requests.Session")
132
+ def test_health_check(self, mock_session_cls):
133
+ mock_session = MagicMock()
134
+ mock_session.get.return_value = _mock_response({"status": "ok"})
135
+ mock_session_cls.return_value = mock_session
136
+ client = _make_client()
137
+ resp = client.health_check()
138
+ mock_session.get.assert_called_with("http://localhost:8317/healthz", timeout=10)
139
+
140
+
141
+ # ============================================================
142
+ # ConfigManager 测试
143
+ # ============================================================
144
+
145
+ class TestConfigManager:
146
+
147
+ def _make_mgr(self):
148
+ client = _make_client()
149
+ return ConfigManager(client)
150
+
151
+ @patch.object(ManagementClient, "get")
152
+ def test_get_config(self, mock_get):
153
+ mock_get.return_value = _mock_response({"port": 8317, "debug": False})
154
+ mgr = self._make_mgr()
155
+ result = mgr.get_config()
156
+ assert result["port"] == 8317
157
+
158
+ @patch.object(ManagementClient, "get")
159
+ def test_get_config_yaml(self, mock_get):
160
+ mock_get.return_value = _mock_response(text_data="port: 8317\n")
161
+ mgr = self._make_mgr()
162
+ result = mgr.get_config_yaml()
163
+ assert "port" in result
164
+
165
+ @patch.object(ManagementClient, "put")
166
+ def test_set_debug(self, mock_put):
167
+ mock_put.return_value = _mock_response({"status": "ok"})
168
+ mgr = self._make_mgr()
169
+ result = mgr.set_debug(True)
170
+ mock_put.assert_called_with("/debug", json_data={"value": True})
171
+
172
+ @patch.object(ManagementClient, "get")
173
+ def test_get_proxy_url(self, mock_get):
174
+ mock_get.return_value = _mock_response({"value": "socks5://proxy:1080"})
175
+ mgr = self._make_mgr()
176
+ result = mgr.get_proxy_url()
177
+ assert result == "socks5://proxy:1080"
178
+
179
+ @patch.object(ManagementClient, "put")
180
+ def test_set_routing_strategy(self, mock_put):
181
+ mock_put.return_value = _mock_response({"status": "ok"})
182
+ mgr = self._make_mgr()
183
+ mgr.set_routing_strategy("fill-first")
184
+ mock_put.assert_called_with("/routing/strategy", json_data={"value": "fill-first"})
185
+
186
+ @patch.object(ManagementClient, "get")
187
+ def test_get_request_retry(self, mock_get):
188
+ mock_get.return_value = _mock_response({"value": 5})
189
+ mgr = self._make_mgr()
190
+ result = mgr.get_request_retry()
191
+ assert result == 5
192
+
193
+ @patch.object(ManagementClient, "get")
194
+ def test_get_ws_auth(self, mock_get):
195
+ mock_get.return_value = _mock_response({"value": True})
196
+ mgr = self._make_mgr()
197
+ assert mgr.get_ws_auth() is True
198
+
199
+ @patch.object(ManagementClient, "get")
200
+ def test_get_latest_version(self, mock_get):
201
+ mock_get.return_value = _mock_response({"version": "6.0.0"})
202
+ mgr = self._make_mgr()
203
+ result = mgr.get_latest_version()
204
+ assert result["version"] == "6.0.0"
205
+
206
+
207
+ # ============================================================
208
+ # AuthManager 测试
209
+ # ============================================================
210
+
211
+ class TestAuthManager:
212
+
213
+ def _make_mgr(self):
214
+ return AuthManager(_make_client())
215
+
216
+ @patch.object(ManagementClient, "get")
217
+ def test_list_auth_files(self, mock_get):
218
+ mock_get.return_value = _mock_response({"files": [{"name": "test.json", "type": "gemini"}]})
219
+ mgr = self._make_mgr()
220
+ result = mgr.list_auth_files()
221
+ assert len(result["files"]) == 1
222
+ assert result["files"][0]["name"] == "test.json"
223
+ mock_get.assert_called_with("/auth-files", params=None)
224
+
225
+ @patch.object(ManagementClient, "get")
226
+ def test_list_auth_files_enabled_only(self, mock_get):
227
+ mock_get.return_value = _mock_response({
228
+ "files": [
229
+ {"name": "disabled.json", "disabled": True},
230
+ {"name": "enabled.json", "disabled": False},
231
+ ]
232
+ })
233
+ mgr = self._make_mgr()
234
+ result = mgr.list_auth_files(disabled=False)
235
+ assert result == {"files": [{"name": "enabled.json", "disabled": False}]}
236
+ mock_get.assert_called_with("/auth-files", params=None)
237
+
238
+ @patch.object(ManagementClient, "get")
239
+ def test_list_auth_files_disabled_only(self, mock_get):
240
+ mock_get.return_value = _mock_response({
241
+ "files": [
242
+ {"name": "disabled.json", "disabled": True},
243
+ {"name": "enabled.json", "disabled": False},
244
+ ]
245
+ })
246
+ mgr = self._make_mgr()
247
+ result = mgr.list_auth_files(disabled=True)
248
+ assert result == {"files": [{"name": "disabled.json", "disabled": True}]}
249
+ mock_get.assert_called_with("/auth-files", params=None)
250
+
251
+ @patch.object(ManagementClient, "post")
252
+ @patch.object(ManagementClient, "get")
253
+ def test_get_codex_quotas(self, mock_get, mock_post):
254
+ mock_get.return_value = _mock_response({
255
+ "files": [
256
+ {
257
+ "name": "codex-active.json",
258
+ "provider": "codex",
259
+ "auth_index": "idx123",
260
+ "disabled": False,
261
+ "email": "user@example.com",
262
+ "id_token": {"chatgpt_account_id": "acct-123", "plan_type": "plus"},
263
+ },
264
+ {
265
+ "name": "codex-disabled.json",
266
+ "provider": "codex",
267
+ "auth_index": "idx456",
268
+ "disabled": True,
269
+ "id_token": {"chatgpt_account_id": "acct-456", "plan_type": "plus"},
270
+ },
271
+ {
272
+ "name": "antigravity.json",
273
+ "provider": "antigravity",
274
+ "auth_index": "idx789",
275
+ "disabled": False,
276
+ },
277
+ ]
278
+ })
279
+ mock_post.return_value = _mock_response({
280
+ "status_code": 200,
281
+ "body": json.dumps({
282
+ "email": "user@example.com",
283
+ "plan_type": "plus",
284
+ "rate_limit": {
285
+ "primary_window": {"used_percent": 21, "limit_window_seconds": 18000, "reset_after_seconds": 4171, "reset_at": 1775939921},
286
+ "secondary_window": {"used_percent": 22, "limit_window_seconds": 604800, "reset_after_seconds": 423178, "reset_at": 1776358928},
287
+ },
288
+ }),
289
+ })
290
+ mgr = self._make_mgr()
291
+ result = mgr.get_codex_quotas()
292
+ assert result["total"] == 1
293
+ assert result["success"] == 1
294
+ assert result["failed"] == 0
295
+ mock_get.assert_called_with("/auth-files", params=None)
296
+ mock_post.assert_called_once_with(
297
+ "/api-call",
298
+ json_data={
299
+ "auth_index": "idx123",
300
+ "method": "GET",
301
+ "url": "https://chatgpt.com/backend-api/wham/usage",
302
+ "header": {
303
+ "Authorization": "Bearer $TOKEN$",
304
+ "Content-Type": "application/json",
305
+ "User-Agent": "codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal",
306
+ "Chatgpt-Account-Id": "acct-123",
307
+ },
308
+ },
309
+ )
310
+ quota = result["quotas"][0]
311
+ assert quota["name"] == "codex-active.json"
312
+ assert quota["plan_type"] == "plus"
313
+ assert quota["primary_window"]["remaining_percent"] == 79
314
+ assert quota["secondary_window"]["remaining_percent"] == 78
315
+
316
+ @patch.object(ManagementClient, "delete")
317
+ def test_delete_auth_file(self, mock_del):
318
+ mock_del.return_value = _mock_response({"status": "ok"})
319
+ mgr = self._make_mgr()
320
+ result = mgr.delete_auth_file("test.json")
321
+ mock_del.assert_called_with("/auth-files", json_data={"filename": "test.json"})
322
+
323
+ @patch.object(ManagementClient, "patch")
324
+ def test_patch_auth_file_status(self, mock_patch):
325
+ mock_patch.return_value = _mock_response({"status": "ok"})
326
+ mgr = self._make_mgr()
327
+ mgr.patch_auth_file_status("test.json", True)
328
+ mock_patch.assert_called_with(
329
+ "/auth-files/status",
330
+ json_data={"filename": "test.json", "disabled": True},
331
+ )
332
+
333
+ @patch.object(ManagementClient, "get")
334
+ def test_get_model_definitions(self, mock_get):
335
+ mock_get.return_value = _mock_response({"models": ["gemini-2.5-pro"]})
336
+ mgr = self._make_mgr()
337
+ result = mgr.get_model_definitions("gemini-cli")
338
+ mock_get.assert_called_with("/model-definitions/gemini-cli")
339
+
340
+ @patch.object(ManagementClient, "get")
341
+ def test_get_auth_file_models(self, mock_get):
342
+ mock_get.return_value = _mock_response({"test.json": ["gemini-2.5-pro"]})
343
+ mgr = self._make_mgr()
344
+ result = mgr.get_auth_file_models()
345
+ assert "test.json" in result
346
+
347
+
348
+ # ============================================================
349
+ # OAuthManager 测试
350
+ # ============================================================
351
+
352
+ class TestOAuthManager:
353
+
354
+ def _make_mgr(self):
355
+ return OAuthManager(_make_client())
356
+
357
+ def test_invalid_provider(self):
358
+ mgr = self._make_mgr()
359
+ with pytest.raises(ValueError, match="不支持提供商"):
360
+ mgr.request_auth_url("invalid_provider")
361
+
362
+ @patch.object(ManagementClient, "get")
363
+ def test_anthropic_login(self, mock_get):
364
+ mock_get.return_value = _mock_response({"auth_url": "https://example.com/oauth"})
365
+ mgr = self._make_mgr()
366
+ result = mgr.request_auth_url("anthropic")
367
+ assert "auth_url" in result
368
+ mock_get.assert_called_with("/anthropic-auth-url", params={})
369
+
370
+ @patch.object(ManagementClient, "get")
371
+ def test_gemini_login(self, mock_get):
372
+ mock_get.return_value = _mock_response({"auth_url": "https://accounts.google.com/..."})
373
+ mgr = self._make_mgr()
374
+ result = mgr.request_auth_url("gemini")
375
+ mock_get.assert_called_with("/gemini-cli-auth-url", params={})
376
+
377
+ @patch.object(ManagementClient, "get")
378
+ def test_login_no_browser(self, mock_get):
379
+ mock_get.return_value = _mock_response({"auth_url": "..."})
380
+ mgr = self._make_mgr()
381
+ mgr.request_auth_url("codex", no_browser=True)
382
+ mock_get.assert_called_with("/codex-auth-url", params={"no_browser": "true"})
383
+
384
+ @patch.object(ManagementClient, "post")
385
+ def test_oauth_callback(self, mock_post):
386
+ mock_post.return_value = _mock_response({"status": "ok"})
387
+ mgr = self._make_mgr()
388
+ mgr.post_oauth_callback("anthropic", "code123", "state456")
389
+ mock_post.assert_called_with(
390
+ "/oauth-callback",
391
+ json_data={"provider": "anthropic", "code": "code123", "state": "state456"},
392
+ )
393
+
394
+ def test_all_providers_exist(self):
395
+ expected = {"anthropic", "codex", "gemini", "antigravity", "qwen", "kimi", "iflow"}
396
+ assert set(OAuthManager.PROVIDERS.keys()) == expected
397
+
398
+
399
+ # ============================================================
400
+ # ModelManager 测试
401
+ # ============================================================
402
+
403
+ class TestModelManager:
404
+
405
+ def _make_mgr(self):
406
+ return ModelManager(_make_client())
407
+
408
+ @patch.object(ManagementClient, "get")
409
+ def test_get_oauth_model_alias(self, mock_get):
410
+ mock_get.return_value = _mock_response({"gemini-cli": [{"name": "gemini-2.5-pro", "alias": "g2.5p"}]})
411
+ mgr = self._make_mgr()
412
+ result = mgr.get_oauth_model_alias()
413
+ assert "gemini-cli" in result
414
+
415
+ @patch.object(ManagementClient, "put")
416
+ def test_put_oauth_model_alias(self, mock_put):
417
+ mock_put.return_value = _mock_response({"status": "ok"})
418
+ mgr = self._make_mgr()
419
+ data = {"claude": [{"name": "claude-sonnet", "alias": "cs"}]}
420
+ mgr.put_oauth_model_alias(data)
421
+ mock_put.assert_called_with("/oauth-model-alias", json_data=data)
422
+
423
+ @patch.object(ManagementClient, "delete")
424
+ def test_delete_oauth_excluded_models(self, mock_del):
425
+ mock_del.return_value = _mock_response({"status": "ok"})
426
+ mgr = self._make_mgr()
427
+ mgr.delete_oauth_excluded_models("claude", "claude-3-haiku")
428
+ mock_del.assert_called_with(
429
+ "/oauth-excluded-models",
430
+ json_data={"channel": "claude", "model": "claude-3-haiku"},
431
+ )
432
+
433
+
434
+ # ============================================================
435
+ # UsageManager 测试
436
+ # ============================================================
437
+
438
+ class TestUsageManager:
439
+
440
+ def _make_mgr(self):
441
+ return UsageManager(_make_client())
442
+
443
+ @patch.object(ManagementClient, "get")
444
+ def test_get_stats(self, mock_get):
445
+ mock_get.return_value = _mock_response({"total_requests": 100})
446
+ mgr = self._make_mgr()
447
+ result = mgr.get_stats()
448
+ assert result["total_requests"] == 100
449
+
450
+ @patch.object(ManagementClient, "get")
451
+ def test_export_stats(self, mock_get):
452
+ mock_get.return_value = _mock_response(text_data='{"exported": true}')
453
+ mgr = self._make_mgr()
454
+ result = mgr.export_stats()
455
+ assert "exported" in result
456
+
457
+
458
+ # ============================================================
459
+ # LogManager 测试
460
+ # ============================================================
461
+
462
+ class TestLogManager:
463
+
464
+ def _make_mgr(self):
465
+ return LogManager(_make_client())
466
+
467
+ @patch.object(ManagementClient, "get")
468
+ def test_get_logs(self, mock_get):
469
+ mock_get.return_value = _mock_response(text_data="line1\nline2\n")
470
+ mgr = self._make_mgr()
471
+ result = mgr.get_logs(lines=50)
472
+ mock_get.assert_called_with("/logs", params={"lines": 50})
473
+
474
+ @patch.object(ManagementClient, "delete")
475
+ def test_delete_logs(self, mock_del):
476
+ mock_del.return_value = _mock_response({"status": "ok"})
477
+ mgr = self._make_mgr()
478
+ result = mgr.delete_logs()
479
+ assert result["status"] == "ok"
480
+
481
+ @patch.object(ManagementClient, "get")
482
+ def test_get_request_error_logs(self, mock_get):
483
+ mock_get.return_value = _mock_response({"logs": ["error1.log"]})
484
+ mgr = self._make_mgr()
485
+ result = mgr.get_request_error_logs()
486
+ assert "logs" in result
487
+
488
+
489
+ # ============================================================
490
+ # APIKeyManager 测试
491
+ # ============================================================
492
+
493
+ class TestAPIKeyManager:
494
+
495
+ def _make_mgr(self):
496
+ return APIKeyManager(_make_client())
497
+
498
+ @patch.object(ManagementClient, "get")
499
+ def test_list_api_keys(self, mock_get):
500
+ mock_get.return_value = _mock_response({"value": ["key1", "key2"]})
501
+ mgr = self._make_mgr()
502
+ result = mgr.list_api_keys()
503
+ assert result == ["key1", "key2"]
504
+
505
+ @patch.object(ManagementClient, "patch")
506
+ def test_add_api_key(self, mock_patch):
507
+ mock_patch.return_value = _mock_response({"status": "ok"})
508
+ mgr = self._make_mgr()
509
+ mgr.add_api_key("newkey")
510
+ mock_patch.assert_called_with("/api-keys", json_data={"value": "newkey"})
511
+
512
+ @patch.object(ManagementClient, "get")
513
+ def test_list_gemini_keys(self, mock_get):
514
+ mock_get.return_value = _mock_response({"value": [{"api_key": "AIza..."}]})
515
+ mgr = self._make_mgr()
516
+ result = mgr.list_gemini_keys()
517
+ assert len(result) == 1
518
+
519
+ @patch.object(ManagementClient, "get")
520
+ def test_list_claude_keys(self, mock_get):
521
+ mock_get.return_value = _mock_response({"value": [{"api_key": "sk-..."}]})
522
+ mgr = self._make_mgr()
523
+ result = mgr.list_claude_keys()
524
+ assert len(result) == 1
525
+
526
+ @patch.object(ManagementClient, "get")
527
+ def test_list_codex_keys(self, mock_get):
528
+ mock_get.return_value = _mock_response({"value": []})
529
+ mgr = self._make_mgr()
530
+ result = mgr.list_codex_keys()
531
+ assert result == []
532
+
533
+ @patch.object(ManagementClient, "get")
534
+ def test_list_openai_compat(self, mock_get):
535
+ mock_get.return_value = _mock_response({"value": [{"name": "openrouter"}]})
536
+ mgr = self._make_mgr()
537
+ result = mgr.list_openai_compat()
538
+ assert result[0]["name"] == "openrouter"
539
+
540
+ @patch.object(ManagementClient, "delete")
541
+ def test_delete_openai_compat(self, mock_del):
542
+ mock_del.return_value = _mock_response({"status": "ok"})
543
+ mgr = self._make_mgr()
544
+ mgr.delete_openai_compat("openrouter")
545
+ mock_del.assert_called_with(
546
+ "/openai-compatibility", json_data={"name": "openrouter"},
547
+ )
548
+
549
+
550
+ # ============================================================
551
+ # ProxyManager 测试
552
+ # ============================================================
553
+
554
+ class TestProxyManager:
555
+
556
+ def _make_mgr(self):
557
+ return ProxyManager(_make_client())
558
+
559
+ @patch.object(ManagementClient, "health_check")
560
+ def test_health_check(self, mock_health):
561
+ mock_health.return_value = _mock_response({"status": "ok"})
562
+ mgr = self._make_mgr()
563
+ result = mgr.health_check()
564
+ assert result["status"] == "ok"
565
+
566
+ @patch.object(ManagementClient, "get")
567
+ def test_get_amp_config(self, mock_get):
568
+ mock_get.return_value = _mock_response({"upstream_url": "https://ampcode.com"})
569
+ mgr = self._make_mgr()
570
+ result = mgr.get_amp_config()
571
+ assert "upstream_url" in result
572
+
573
+ @patch.object(ManagementClient, "get")
574
+ def test_get_amp_model_mappings(self, mock_get):
575
+ mock_get.return_value = _mock_response({"value": [{"from": "claude-opus", "to": "gemini-pro"}]})
576
+ mgr = self._make_mgr()
577
+ result = mgr.get_amp_model_mappings()
578
+ assert len(result) == 1
579
+
580
+ @patch.object(ManagementClient, "post")
581
+ def test_api_call(self, mock_post):
582
+ mock_post.return_value = _mock_response({
583
+ "status_code": 200,
584
+ "body": '{"result": true}',
585
+ })
586
+ mgr = self._make_mgr()
587
+ result = mgr.api_call("GET", "https://api.example.com/test")
588
+ assert result["status_code"] == 200
589
+ mock_post.assert_called_with(
590
+ "/api-call",
591
+ json_data={"method": "GET", "url": "https://api.example.com/test"},
592
+ )
593
+
594
+ @patch.object(ManagementClient, "post")
595
+ def test_api_call_with_auth_index(self, mock_post):
596
+ mock_post.return_value = _mock_response({"status_code": 200, "body": ""})
597
+ mgr = self._make_mgr()
598
+ mgr.api_call("GET", "https://api.example.com/", auth_index="idx123")
599
+ call_args = mock_post.call_args
600
+ assert call_args[1]["json_data"]["auth_index"] == "idx123"
601
+
602
+
603
+ # ============================================================
604
+ # Output 工具测试
605
+ # ============================================================
606
+
607
+ class TestOutputUtils:
608
+
609
+ def test_output_json(self):
610
+ from cli_anything.cliproxyapi.utils.output import output_json
611
+ import io
612
+ from rich.console import Console
613
+
614
+ console_buf = io.StringIO()
615
+ c = Console(file=console_buf)
616
+ # 简单验证函数不抛异常
617
+ output_json({"key": "value"})
618
+
619
+ def test_output_error(self):
620
+ from cli_anything.cliproxyapi.utils.output import output_error
621
+ output_error("test error")