""" BitgetTradingAPI.get_balance() 单元测试 覆盖场景: 1. UTA 模式 → privateUtaGetV3AccountAssets(真实路径) 2. 经典账户模式 → privateMixGetV2MixAccountAccounts 3. API 异常 → 返回 {} 4. 空 assets / 无 USDT 5. get_account_state 端到端解析 """ import importlib.util import os import sys import types from pathlib import Path from unittest.mock import MagicMock sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) def load_bitget_sdk_class(): sdk_path = ( Path(__file__).resolve().parents[1] / 'app' / 'services' / 'bitget_trading_api_sdk.py' ) if 'ccxt' not in sys.modules: ccxt_module = types.ModuleType('ccxt') ccxt_module.BaseError = Exception ccxt_module.bitget = MagicMock() sys.modules['ccxt'] = ccxt_module if 'app.config' not in sys.modules: mock_cfg = types.ModuleType('app.config') mock_cfg.get_settings = MagicMock() sys.modules['app.config'] = mock_cfg if 'app.utils.logger' not in sys.modules: mock_log = types.ModuleType('app.utils.logger') mock_log.logger = MagicMock() sys.modules['app.utils.logger'] = mock_log module_name = 'app.services.bitget_trading_api_sdk_bal_test' spec = importlib.util.spec_from_file_location(module_name, sdk_path) module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module spec.loader.exec_module(module) return module.BitgetTradingAPI # ────────────────────────────────────────────── # 测试 1: UTA 模式,正常返回(匹配真实 API) # ────────────────────────────────────────────── def test_uta_balance_success(): """UTA 模式 → privateUtaGetV3AccountAssets 返回 1975.37""" BitgetTradingAPI = load_bitget_sdk_class() api = BitgetTradingAPI.__new__(BitgetTradingAPI) api.exchange = MagicMock() api.use_unified_account = True api.exchange.privateUtaGetV3AccountAssets.return_value = { 'code': '00000', 'data': { 'assets': [ { 'coin': 'USDT', 'available': '1975.37457492', 'locked': '0', 'equity': '1975.37457492', } ], }, } balance = api.get_balance() assert balance['USDT']['available'] == '1975.37457492' assert balance['USDT']['equity'] == '1975.37457492' assert balance['USDT']['frozen'] == '0' api.exchange.privateUtaGetV3AccountAssets.assert_called_once_with({'coin': 'USDT'}) # ────────────────────────────────────────────── # 测试 2: UTA 多币种返回 # ────────────────────────────────────────────── def test_uta_multi_coins(): """UTA 返回多个币种,正确解析""" BitgetTradingAPI = load_bitget_sdk_class() api = BitgetTradingAPI.__new__(BitgetTradingAPI) api.exchange = MagicMock() api.use_unified_account = True api.exchange.privateUtaGetV3AccountAssets.return_value = { 'code': '00000', 'data': { 'assets': [ {'coin': 'USDT', 'available': '1000.00', 'locked': '200.00', 'equity': '1200.00'}, {'coin': 'BGB', 'available': '50.00', 'locked': '0', 'equity': '50.00'}, ], }, } balance = api.get_balance() assert balance['USDT']['available'] == '1000.00' assert balance['USDT']['frozen'] == '200.00' assert balance['BGB']['available'] == '50.00' # ────────────────────────────────────────────── # 测试 3: 经典合约账户模式 # ────────────────────────────────────────────── def test_contract_account_balance(): """经典账户 → privateMixGetV2MixAccountAccounts""" BitgetTradingAPI = load_bitget_sdk_class() api = BitgetTradingAPI.__new__(BitgetTradingAPI) api.exchange = MagicMock() api.use_unified_account = False api.exchange.privateMixGetV2MixAccountAccounts.return_value = { 'code': '00000', 'data': [ { 'marginCoin': 'USDT', 'available': '800.50', 'locked': '100.00', 'accountEquity': '900.50', } ], } balance = api.get_balance() assert balance['USDT']['available'] == '800.50' assert balance['USDT']['frozen'] == '100.00' assert balance['USDT']['equity'] == '900.50' # ────────────────────────────────────────────── # 测试 4: API 异常 → 返回 {} # ────────────────────────────────────────────── def test_api_exception_returns_empty(): """API 抛异常 → 返回空字典""" BitgetTradingAPI = load_bitget_sdk_class() api = BitgetTradingAPI.__new__(BitgetTradingAPI) api.exchange = MagicMock() api.use_unified_account = True api.exchange.privateUtaGetV3AccountAssets.side_effect = Exception("network error") balance = api.get_balance() assert balance == {} # ────────────────────────────────────────────── # 测试 5: UTA 空 assets → 返回 {} # ────────────────────────────────────────────── def test_uta_empty_assets(): """UTA API 返回空 assets 列表""" BitgetTradingAPI = load_bitget_sdk_class() api = BitgetTradingAPI.__new__(BitgetTradingAPI) api.exchange = MagicMock() api.use_unified_account = True api.exchange.privateUtaGetV3AccountAssets.return_value = { 'code': '00000', 'data': {'assets': []}, } balance = api.get_balance() assert balance == {} # ────────────────────────────────────────────── # 测试 6: UTA assets 无 coin 字段 → 跳过 # ────────────────────────────────────────────── def test_uta_skip_no_coin(): """assets 中 entry 无 coin → 跳过该条目""" BitgetTradingAPI = load_bitget_sdk_class() api = BitgetTradingAPI.__new__(BitgetTradingAPI) api.exchange = MagicMock() api.use_unified_account = True api.exchange.privateUtaGetV3AccountAssets.return_value = { 'code': '00000', 'data': { 'assets': [ {'coin': '', 'available': '100'}, {'coin': 'USDT', 'available': '500.00', 'locked': '0', 'equity': '500.00'}, ], }, } balance = api.get_balance() assert 'USDT' in balance assert balance['USDT']['available'] == '500.00' assert len(balance) == 1 # ────────────────────────────────────────────── # 测试 7: UTA assets 中字段为 None → 解析为 '0' # ────────────────────────────────────────────── def test_uta_none_fields(): """UTA assets 中 available/locked 为 None → 解析为 '0'""" BitgetTradingAPI = load_bitget_sdk_class() api = BitgetTradingAPI.__new__(BitgetTradingAPI) api.exchange = MagicMock() api.use_unified_account = True api.exchange.privateUtaGetV3AccountAssets.return_value = { 'code': '00000', 'data': { 'assets': [ {'coin': 'USDT', 'available': None, 'locked': None, 'equity': None}, ], }, } balance = api.get_balance() assert balance['USDT']['available'] == '0' # _pick_first_value 对 None 返回默认值 '0' # ────────────────────────────────────────────── # 测试 8: get_account_state 端到端解析(正常余额) # ────────────────────────────────────────────── def test_get_account_state_e2e(): """get_account_state 正确解析 balance 返回值""" balance_return = { 'USDT': { 'available': '1975.50', 'frozen': '24.50', 'locked': '24.50', 'equity': '2000.00', } } usdt = balance_return.get('USDT', {}) available = float(usdt.get('available', 0) or 0) frozen = float(usdt.get('frozen', 0) or 0) equity = float(usdt.get('equity', 0) or 0) account_value = equity if equity > 0 else (available + frozen) assert available == 1975.50 assert frozen == 24.50 assert equity == 2000.00 assert account_value == 2000.00 # ────────────────────────────────────────────── # 测试 9: balance 返回 {} 时 get_account_state 各字段为 0 # ────────────────────────────────────────────── def test_get_account_state_empty_balance(): """balance 返回空 → 全部为 0""" balance_return = {} usdt = balance_return.get('USDT', {}) available = float(usdt.get('available', 0) or 0) frozen = float(usdt.get('frozen', 0) or 0) equity = float(usdt.get('equity', 0) or 0) account_value = equity if equity > 0 else (available + frozen) assert available == 0.0 assert frozen == 0.0 assert account_value == 0.0 # ────────────────────────────────────────────── # 测试 10: UTA 返回 data=None → 返回 {} # ────────────────────────────────────────────── def test_uta_data_none(): """UTA API 返回 data=None""" BitgetTradingAPI = load_bitget_sdk_class() api = BitgetTradingAPI.__new__(BitgetTradingAPI) api.exchange = MagicMock() api.use_unified_account = True api.exchange.privateUtaGetV3AccountAssets.return_value = { 'code': '00000', 'data': None, } balance = api.get_balance() assert balance == {}