"""app.py main() 和 config_cmd.py 单元测试。 运行: python tests/unit/test_app_config.py """ from __future__ import annotations import io import os import sys import tempfile import unittest from unittest.mock import MagicMock, patch, call sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "assets")) from sapcli.cli.app import main from sapcli.commands.config_cmd import cmd_config from sapcli.exceptions import ConfigError, SapCliError # ────────────────────────────────────────────────────────────────────── # 辅助:构造一个假的 SAPConfig # ────────────────────────────────────────────────────────────────────── def _fake_config(): """返回一个 (SAPConfig, loaded_from) 元组。""" from sapcli.config import SAPConfig cfg = SAPConfig(host="sap.example.com", client="100", user="TESTUSER", password="secret123") return cfg, "/tmp/fake_config.ini" # ────────────────────────────────────────────────────────────────────── # app.py main() 测试 # ────────────────────────────────────────────────────────────────────── class TestMainNoCommand(unittest.TestCase): """无子命令时 print_help + exit(1)。""" @patch("sapcli.cli.app._suppress_ssl_warnings") @patch("sapcli.cli.app._setup_logging") @patch("sapcli.cli.app._setup_encoding") def test_main_no_command(self, mock_enc, mock_log, mock_ssl): with patch("sys.argv", ["sap-cli"]): with self.assertRaises(SystemExit) as ctx: main() self.assertEqual(ctx.exception.code, 1) class TestMainConfigShow(unittest.TestCase): """config 命令 → cmd_config 被调用。""" @patch("sapcli.cli.app.cmd_config") @patch("sapcli.cli.app._suppress_ssl_warnings") @patch("sapcli.cli.app._setup_logging") @patch("sapcli.cli.app._setup_encoding") def test_main_config_show(self, mock_enc, mock_log, mock_ssl, mock_cmd_config): with patch("sys.argv", ["sap-cli", "config", "show"]): main() mock_cmd_config.assert_called_once() # 第一个位置参数是 args (Namespace) args_passed = mock_cmd_config.call_args[0][0] self.assertEqual(args_passed.command, "config") class TestMainAuthLogin(unittest.TestCase): """auth login → cmd_auth_login 被调用。""" @patch("sapcli.cli.app.cmd_auth_login") @patch("sapcli.cli.app._suppress_ssl_warnings") @patch("sapcli.cli.app._setup_logging") @patch("sapcli.cli.app._setup_encoding") def test_main_auth_login(self, mock_enc, mock_log, mock_ssl, mock_login): with patch("sys.argv", ["sap-cli", "auth", "login"]): main() mock_login.assert_called_once() class TestMainAuthLogout(unittest.TestCase): """auth logout → cmd_auth_logout 被调用。""" @patch("sapcli.cli.app.cmd_auth_logout") @patch("sapcli.cli.app._suppress_ssl_warnings") @patch("sapcli.cli.app._setup_logging") @patch("sapcli.cli.app._setup_encoding") def test_main_auth_logout(self, mock_enc, mock_log, mock_ssl, mock_logout): with patch("sys.argv", ["sap-cli", "auth", "logout"]): main() mock_logout.assert_called_once() class TestMainAuthStatus(unittest.TestCase): """auth status (无子命令) → cmd_auth_status 被调用。""" @patch("sapcli.cli.app.cmd_auth_status") @patch("sapcli.cli.app._suppress_ssl_warnings") @patch("sapcli.cli.app._setup_logging") @patch("sapcli.cli.app._setup_encoding") def test_main_auth_status(self, mock_enc, mock_log, mock_ssl, mock_status): with patch("sys.argv", ["sap-cli", "auth", "status"]): main() mock_status.assert_called_once() class TestMainConfigError(unittest.TestCase): """load_config 抛 ConfigError → exit(1)。""" @patch("sapcli.cli.app.load_config", side_effect=ConfigError("配置缺失")) @patch("sapcli.cli.app._suppress_ssl_warnings") @patch("sapcli.cli.app._setup_logging") @patch("sapcli.cli.app._setup_encoding") def test_main_config_error(self, mock_enc, mock_log, mock_ssl, mock_load): with patch("sys.argv", ["sap-cli", "download", "--name", "ZT", "--type", "report", "--path", "./out"]): with self.assertRaises(SystemExit) as ctx: main() self.assertEqual(ctx.exception.code, 1) class TestMainDownloadSuccess(unittest.TestCase): """download 命令全链路 mock:load_config → ADTClient → login → cmd_download。""" @patch("sapcli.cli.app.cmd_download") @patch("sapcli.cli.app.ADTClient") @patch("sapcli.cli.app.load_config", return_value=_fake_config()) @patch("sapcli.cli.app._suppress_ssl_warnings") @patch("sapcli.cli.app._setup_logging") @patch("sapcli.cli.app._setup_encoding") def test_main_download_success(self, mock_enc, mock_log, mock_ssl, mock_load, mock_adt_cls, mock_download): mock_client = MagicMock() mock_adt_cls.return_value = mock_client with patch("sys.argv", ["sap-cli", "download", "--name", "ZTEST", "--type", "report", "--path", "./out"]): main() # 验证 ADTClient 被正确构造 mock_adt_cls.assert_called_once_with( "sap.example.com", "100", "TESTUSER", "secret123", verify_ssl=False ) # 验证 login 被调用 mock_client.login.assert_called_once() # 验证 cmd_download 被调用 mock_download.assert_called_once() # cmd_download 的第二个参数应该是 mock_client self.assertIs(mock_download.call_args[0][1], mock_client) class TestMainSapCliError(unittest.TestCase): """handler 抛 SapCliError → exit(1)。""" @patch("sapcli.cli.app.cmd_download", side_effect=SapCliError("下载失败")) @patch("sapcli.cli.app.ADTClient") @patch("sapcli.cli.app.load_config", return_value=_fake_config()) @patch("sapcli.cli.app._suppress_ssl_warnings") @patch("sapcli.cli.app._setup_logging") @patch("sapcli.cli.app._setup_encoding") def test_main_sap_cli_error(self, mock_enc, mock_log, mock_ssl, mock_load, mock_adt_cls, mock_download): mock_adt_cls.return_value = MagicMock() with patch("sys.argv", ["sap-cli", "download", "--name", "ZTEST", "--type", "report", "--path", "./out"]): with self.assertRaises(SystemExit) as ctx: main() self.assertEqual(ctx.exception.code, 1) class TestMainVerifySslFlag(unittest.TestCase): """--verify-ssl 参数传递给 ADTClient。""" @patch("sapcli.cli.app.cmd_download") @patch("sapcli.cli.app.ADTClient") @patch("sapcli.cli.app.load_config", return_value=_fake_config()) @patch("sapcli.cli.app._suppress_ssl_warnings") @patch("sapcli.cli.app._setup_logging") @patch("sapcli.cli.app._setup_encoding") def test_main_verify_ssl_flag(self, mock_enc, mock_log, mock_ssl, mock_load, mock_adt_cls, mock_download): mock_adt_cls.return_value = MagicMock() with patch("sys.argv", [ "sap-cli", "--verify-ssl", "download", "--name", "ZTEST", "--type", "report", "--path", "./out", ]): main() # verify_ssl 应为 True mock_adt_cls.assert_called_once_with( "sap.example.com", "100", "TESTUSER", "secret123", verify_ssl=True ) class TestMainProfileFlag(unittest.TestCase): """--profile 参数传递给 load_config。""" @patch("sapcli.cli.app.cmd_download") @patch("sapcli.cli.app.ADTClient") @patch("sapcli.cli.app.load_config", return_value=_fake_config()) @patch("sapcli.cli.app._suppress_ssl_warnings") @patch("sapcli.cli.app._setup_logging") @patch("sapcli.cli.app._setup_encoding") def test_main_profile_flag(self, mock_enc, mock_log, mock_ssl, mock_load, mock_adt_cls, mock_download): mock_adt_cls.return_value = MagicMock() with patch("sys.argv", [ "sap-cli", "--profile", "DEV", "download", "--name", "ZTEST", "--type", "report", "--path", "./out", ]): main() # load_config 应收到 profile="DEV" mock_load.assert_called_once() # load_config(config_path, profile=...) # parser 中 --config 的 dest 是 "config",--profile 的 dest 是 "profile" # load_config 调用签名: load_config(args.config, profile=getattr(args, "profile", None)) call_kwargs = mock_load.call_args self.assertEqual(call_kwargs[0][0], None) # args.config = None # 第二个参数是 profile # load_config 被调用为 load_config(args.config, profile=args.profile) # 所以要看 kwargs 或位置参数 if call_kwargs[1]: self.assertEqual(call_kwargs[1].get("profile"), "DEV") else: # 可能是位置参数方式传递 self.assertEqual(call_kwargs[0][1], "DEV") # ────────────────────────────────────────────────────────────────────── # config_cmd.py 测试 # ────────────────────────────────────────────────────────────────────── class TestConfigShowDefault(unittest.TestCase): """config show → 显示默认配置。""" # _config_show 内部用 from sapcli.config import load_config,所以 patch 源模块 @patch("sapcli.config.load_config", return_value=_fake_config()) def test_config_show_default(self, mock_load): import argparse args = argparse.Namespace(config=None, config_action="show") # 捕获 stdout with patch("sys.stdout", new_callable=io.StringIO) as mock_out: cmd_config(args) output = mock_out.getvalue() self.assertIn("sap-cli 当前配置", output) self.assertIn("sap.example.com", output) self.assertIn("100", output) self.assertIn("TESTUSER", output) self.assertIn("***", output) class TestConfigListProfiles(unittest.TestCase): """config list-profiles → 显示多 profile。""" def test_config_list_profiles(self): import argparse import configparser # 创建一个临时配置文件包含多个 section with tempfile.NamedTemporaryFile(mode="w", suffix=".ini", delete=False, encoding="utf-8") as f: parser = configparser.ConfigParser() parser.add_section("SAP") parser.set("SAP", "host", "sap.example.com") parser.add_section("DEV") parser.set("DEV", "host", "dev.sap.example.com") parser.write(f) tmp_path = f.name try: args = argparse.Namespace(config=tmp_path, config_action="list-profiles") with patch("sys.stdout", new_callable=io.StringIO) as mock_out: cmd_config(args) output = mock_out.getvalue() self.assertIn("sap-cli 可用 profile", output) self.assertIn("SAP", output) self.assertIn("DEV", output) self.assertIn("(默认)", output) # SAP section 是默认的 finally: os.unlink(tmp_path) class TestConfigSetWritesFile(unittest.TestCase): """config set → 写入配置文件。""" def test_config_set_writes_file(self): import argparse import configparser with tempfile.NamedTemporaryFile(mode="w", suffix=".ini", delete=False, encoding="utf-8") as f: f.write("") tmp_path = f.name try: args = argparse.Namespace( config=tmp_path, config_action="set", key="host", value="newhost.example.com", profile=None, ) with patch("sys.stdout", new_callable=io.StringIO): cmd_config(args) # 验证文件已写入 parser = configparser.ConfigParser() parser.read(tmp_path, encoding="utf-8") self.assertTrue(parser.has_section("SAP")) self.assertEqual(parser.get("SAP", "host"), "newhost.example.com") finally: os.unlink(tmp_path) class TestConfigSetInvalidKey(unittest.TestCase): """config set 无效 key → 报错。""" def test_config_set_invalid_key(self): import argparse args = argparse.Namespace( config=None, config_action="set", key="invalid_key", value="some_value", profile=None, ) with patch("sys.stdout", new_callable=io.StringIO) as mock_out: cmd_config(args) output = mock_out.getvalue() self.assertIn("不支持的配置项", output) if __name__ == "__main__": unittest.main()