"""CLI tests for CoinHunter V2.""" from __future__ import annotations import io import unittest from unittest.mock import patch from coinhunter import cli class CLITestCase(unittest.TestCase): def test_help_includes_v2_commands(self): parser = cli.build_parser() help_text = parser.format_help() self.assertIn("init", help_text) self.assertIn("account", help_text) self.assertIn("buy", help_text) self.assertIn("sell", help_text) self.assertIn("opportunity", help_text) self.assertIn("--doc", help_text) def test_init_dispatches(self): captured = {} with ( patch.object(cli, "ensure_init_files", return_value={"force": True, "root": "/tmp/ch"}), patch.object( cli, "install_shell_completion", return_value={"shell": "zsh", "installed": True, "path": "/tmp/ch/_coinhunter"}, ), patch.object( cli, "print_output", side_effect=lambda payload, **kwargs: captured.setdefault("payload", payload) ), ): result = cli.main(["init", "--force"]) self.assertEqual(result, 0) self.assertTrue(captured["payload"]["force"]) self.assertIn("completion", captured["payload"]) def test_old_command_is_rejected(self): with self.assertRaises(SystemExit): cli.main(["exec", "bal"]) def test_runtime_error_is_rendered_cleanly(self): stderr = io.StringIO() with patch.object(cli, "load_config", side_effect=RuntimeError("boom")), patch("sys.stderr", stderr): result = cli.main(["market", "tickers", "BTCUSDT"]) self.assertEqual(result, 1) self.assertIn("error: boom", stderr.getvalue()) def test_buy_dispatches(self): captured = {} with patch.object(cli, "load_config", return_value={"binance": {"spot_base_url": "https://test", "recv_window": 5000}, "trading": {"dry_run_default": True}}), patch.object( cli, "get_binance_credentials", return_value={"api_key": "k", "api_secret": "s"} ), patch.object( cli, "SpotBinanceClient" ), patch.object( cli.trade_service, "execute_spot_trade", return_value={"trade": {"status": "DRY_RUN"}} ), patch.object( cli, "print_output", side_effect=lambda payload, **kwargs: captured.setdefault("payload", payload) ): result = cli.main(["buy", "BTCUSDT", "-Q", "100"]) self.assertEqual(result, 0) self.assertEqual(captured["payload"]["trade"]["status"], "DRY_RUN") def test_sell_dispatches(self): captured = {} with patch.object(cli, "load_config", return_value={"binance": {"spot_base_url": "https://test", "recv_window": 5000}, "trading": {"dry_run_default": True}}), patch.object( cli, "get_binance_credentials", return_value={"api_key": "k", "api_secret": "s"} ), patch.object( cli, "SpotBinanceClient" ), patch.object( cli.trade_service, "execute_spot_trade", return_value={"trade": {"status": "DRY_RUN"}} ), patch.object( cli, "print_output", side_effect=lambda payload, **kwargs: captured.setdefault("payload", payload) ): result = cli.main(["sell", "BTCUSDT", "-q", "0.01"]) self.assertEqual(result, 0) self.assertEqual(captured["payload"]["trade"]["status"], "DRY_RUN") def test_doc_flag_prints_documentation(self): import io from unittest.mock import patch stdout = io.StringIO() with patch("sys.stdout", stdout): result = cli.main(["market", "tickers", "--doc"]) self.assertEqual(result, 0) output = stdout.getvalue() self.assertIn("lastPrice", output) self.assertIn("BTCUSDT", output) def test_account_dispatches(self): captured = {} with ( patch.object( cli, "load_config", return_value={"binance": {"spot_base_url": "https://test", "recv_window": 5000}, "market": {"default_quote": "USDT"}, "trading": {"dust_usdt_threshold": 10.0}} ), patch.object(cli, "get_binance_credentials", return_value={"api_key": "k", "api_secret": "s"}), patch.object(cli, "SpotBinanceClient"), patch.object( cli.account_service, "get_balances", return_value={"balances": [{"asset": "BTC", "is_dust": False}]} ), patch.object( cli, "print_output", side_effect=lambda payload, **kwargs: captured.setdefault("payload", payload) ), ): result = cli.main(["account"]) self.assertEqual(result, 0) self.assertEqual(captured["payload"]["balances"][0]["asset"], "BTC") def test_upgrade_dispatches(self): captured = {} with ( patch.object(cli, "self_upgrade", return_value={"command": "pipx upgrade coinhunter", "returncode": 0}), patch.object( cli, "print_output", side_effect=lambda payload, **kwargs: captured.setdefault("payload", payload) ), ): result = cli.main(["upgrade"]) self.assertEqual(result, 0) self.assertEqual(captured["payload"]["returncode"], 0) def test_catlog_dispatches(self): captured = {} with ( patch.object( cli, "read_audit_log", return_value=[{"timestamp": "2026-04-17T12:00:00Z", "event": "test_event"}] ), patch.object( cli, "print_output", side_effect=lambda payload, **kwargs: captured.setdefault("payload", payload) ), ): result = cli.main(["catlog", "-n", "5", "-o", "10"]) self.assertEqual(result, 0) self.assertEqual(captured["payload"]["limit"], 5) self.assertEqual(captured["payload"]["offset"], 10) self.assertIn("entries", captured["payload"]) self.assertEqual(captured["payload"]["total"], 1)