Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth #2822

Merged
merged 2 commits into from
Dec 7, 2024
Merged

Auth #2822

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion additional_tests/exchanges_tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def get_name(self):
async def get_authenticated_exchange_manager(
exchange_name, exchange_tentacle_name, config=None,
credentials_exchange_name=None, market_filter=None,
use_invalid_creds=False
use_invalid_creds=False, http_proxy_callback_factory=None
):
credentials_exchange_name = credentials_exchange_name or exchange_name
_load_exchange_creds_env_variables_if_necessary()
Expand All @@ -89,6 +89,9 @@ async def get_authenticated_exchange_manager(
.enable_storage(False) \
.disable_trading_mode() \
.is_exchange_only()
if http_proxy_callback_factory:
proxy_callback = http_proxy_callback_factory(exchange_builder.exchange_manager)
exchange_builder.set_proxy_config(exchanges.ProxyConfig(http_proxy_callback=proxy_callback))
exchange_manager_instance = await exchange_builder.build()
# create trader afterwards to init exchange personal data
exchange_manager_instance.trader.is_enabled = True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
import contextlib
import decimal
import time

import typing
import ccxt
import mock
import pytest

import octobot_commons.constants as constants
import octobot_commons.enums as commons_enums
import octobot_commons.symbols as symbols
import octobot_trading.errors as trading_errors
import octobot_trading.enums as trading_enums
Expand Down Expand Up @@ -80,6 +82,7 @@ class AbstractAuthenticatedExchangeTester:
MAX_TRADE_USD_VALUE = decimal.Decimal(8000)
MIN_TRADE_USD_VALUE = decimal.Decimal("0.1")
IS_ACCOUNT_ID_AVAILABLE = True # set False when get_account_id is not available and should be checked
IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE = False # set True when is_authenticated_request is implemented
EXPECTED_GENERATED_ACCOUNT_ID = False # set True when account_id can't be fetch and a generated account id is used
USE_ORDER_OPERATION_TO_CHECK_API_KEY_RIGHTS = False # set True when api key rights can't be checked using a
# dedicated api and have to be checked by sending an order operation
Expand Down Expand Up @@ -142,7 +145,7 @@ async def test_get_account_id(self):
await self.inner_test_get_account_id()

async def inner_test_get_account_id(self):
if self.IS_ACCOUNT_ID_AVAILABLE:
if self.IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE:
account_id = await self.exchange_manager.exchange.get_account_id()
assert account_id
assert isinstance(account_id, str)
Expand All @@ -154,6 +157,104 @@ async def inner_test_get_account_id(self):
with pytest.raises(NotImplementedError):
await self.exchange_manager.exchange.get_account_id()

async def test_is_authenticated_request(self):
rest_exchange_data = {
"calls": [],
}
def _http_proxy_callback_factory(_exchange_manager):
rest_exchange_data["exchange_manager"] = _exchange_manager
def proxy_callback(url: str, method: str, headers: dict, body) -> typing.Optional[str]:
if not self.IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE:
return None
if "rest_exchange" not in rest_exchange_data:
rest_exchange_data["rest_exchange"] = rest_exchange_data["exchange_manager"].exchange
rest_exchange = rest_exchange_data["rest_exchange"]
exchange_return_value = rest_exchange.is_authenticated_request(url, method, headers, body)
rest_exchange_data["calls"].append(
((url, method, headers, body), exchange_return_value)
)
# never return any proxy url: no proxy is set but make sure to register each request call
return None

return proxy_callback

async with self.local_exchange_manager(http_proxy_callback_factory=_http_proxy_callback_factory):
await self.inner_is_authenticated_request(rest_exchange_data)

async def inner_is_authenticated_request(self, rest_exchange_data):
if self.IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE:
latest_call_indexes = [len(rest_exchange_data["calls"])]
def get_latest_calls():
latest_calls = rest_exchange_data["calls"][latest_call_indexes[-1]:]
latest_call_indexes.append(len(rest_exchange_data["calls"]))
return latest_calls

def assert_has_at_least_one_authenticated_call(calls):
has_authenticated_call = False
for latest_call in calls:
# should be at least 1 authenticated call
if latest_call[1] is True:
has_authenticated_call = True
assert has_authenticated_call, f"{calls} should contain at last 1 authenticated call" # authenticated request

# 1. test using different values
assert self.exchange_manager.exchange.is_authenticated_request("", "", {}, None) is False
assert self.exchange_manager.exchange.is_authenticated_request("", "", {}, "") is False
assert self.exchange_manager.exchange.is_authenticated_request("", "", {}, b"") is False
assert self.exchange_manager.exchange.is_authenticated_request(None, None, None, b"") is False

# 2. make public requests
assert await self.exchange_manager.exchange.get_symbol_prices(
self.SYMBOL, commons_enums.TimeFrames.ONE_HOUR
)
latest_calls = get_latest_calls()
for latest_call in latest_calls:
assert latest_call[1] is False, f"{latest_call} should be NOT authenticated" # authenticated request
ticker = await self.exchange_manager.exchange.get_price_ticker(self.SYMBOL)
assert ticker
last_price = ticker[trading_enums.ExchangeConstantsTickersColumns.CLOSE.value]
assert rest_exchange_data["calls"][-1][0][0] != latest_calls[-1][0][0] # assert latest call's url changed
latest_calls = get_latest_calls()
for latest_call in latest_calls:
assert latest_call[1] is False, f"{latest_call} should be NOT authenticated" # authenticated request

# 3. make private requests
# balance (usually a GET)
portfolio = await self.get_portfolio()
if self.CHECK_EMPTY_ACCOUNT:
assert portfolio == {}
else:
assert portfolio
assert rest_exchange_data["calls"][-1][0][0] != latest_calls[-1][0][0] # assert latest call's url changed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

latest_calls = get_latest_calls()
assert_has_at_least_one_authenticated_call(latest_calls)
# create order (usually a POST)
price = decimal.Decimal(str(last_price)) * decimal.Decimal("0.7")
amount = self.get_order_size(
portfolio, price, symbol=self.SYMBOL, settlement_currency=self.SETTLEMENT_CURRENCY
) * 100000
if self.CHECK_EMPTY_ACCOUNT:
amount = 10
# (amount is too large, creating buy order will fail)
with pytest.raises(ccxt.ExchangeError):
await self.exchange_manager.exchange.connector.create_limit_buy_order(
self.SYMBOL, amount, price=price, params={}
)
assert rest_exchange_data["calls"][-1][0][0] != latest_calls[-1][0][0] # assert latest call's url changed
latest_calls = get_latest_calls()
assert_has_at_least_one_authenticated_call(latest_calls)
# cancel order (usually a DELETE)
with pytest.raises(ccxt.BaseError):
# use client call directly to avoid any octobot error conversion
await self.exchange_manager.exchange.connector.client.cancel_order(self.VALID_ORDER_ID, self.SYMBOL)
assert rest_exchange_data["calls"][-1][0][0] != latest_calls[-1][0][0] # assert latest call's url changed
latest_calls = get_latest_calls()
assert_has_at_least_one_authenticated_call(latest_calls)
else:
with pytest.raises(NotImplementedError):
self.exchange_manager.exchange.is_authenticated_request("", "", {}, None)
await asyncio.sleep(1.5) # let initial requests finish to be able to stop exchange manager

async def test_invalid_api_key_error(self):
with pytest.raises(trading_errors.AuthenticationError):
created_exchange = mock.Mock()
Expand Down Expand Up @@ -787,7 +888,9 @@ async def get_price(self, symbol=None):
))

@contextlib.asynccontextmanager
async def local_exchange_manager(self, market_filter=None, identifiers_suffix=None, use_invalid_creds=False):
async def local_exchange_manager(
self, market_filter=None, identifiers_suffix=None, use_invalid_creds=False, http_proxy_callback_factory=None
):
try:
exchange_tentacle_name = self.EXCHANGE_TENTACLE_NAME or self.EXCHANGE_NAME.capitalize()
credentials_exchange_name = self.CREDENTIALS_EXCHANGE_NAME or self.EXCHANGE_NAME
Expand All @@ -800,6 +903,7 @@ async def local_exchange_manager(self, market_filter=None, identifiers_suffix=No
credentials_exchange_name=credentials_exchange_name,
market_filter=market_filter,
use_invalid_creds=use_invalid_creds,
http_proxy_callback_factory=http_proxy_callback_factory,
) as exchange_manager:
self.exchange_manager = exchange_manager
yield
Expand Down
3 changes: 3 additions & 0 deletions additional_tests/exchanges_tests/test_ascendex.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ async def test_get_account_id(self):
# pass if not implemented
pass

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
5 changes: 5 additions & 0 deletions additional_tests/exchanges_tests/test_binance.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class TestBinanceAuthenticatedExchange(
'7457370420',
]
IS_BROKER_ENABLED_ACCOUNT = False
IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE = True # set True when is_authenticated_request is implemented


async def test_get_portfolio(self):
await super().test_get_portfolio()
Expand All @@ -49,6 +51,9 @@ async def test_get_portfolio_with_market_filter(self):
async def test_get_account_id(self):
await super().test_get_account_id()

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
4 changes: 4 additions & 0 deletions additional_tests/exchanges_tests/test_binance_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class TestBinanceFuturesAuthenticatedExchange(
DUPLICATE_TRADES_RATIO = 0.1 # allow 10% duplicate in trades (due to trade id set to order id)
VALID_ORDER_ID = "26408108410"
EXPECTED_QUOTE_MIN_ORDER_SIZE = 200 # min quote value of orders to create (used to check market status parsing)
IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE = True # set True when is_authenticated_request is implemented

async def _set_account_types(self, account_types):
# todo remove this and use both types when exchange-side multi portfolio is enabled
Expand All @@ -51,6 +52,9 @@ async def test_get_portfolio_with_market_filter(self):
async def test_get_account_id(self):
await super().test_get_account_id()

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
4 changes: 4 additions & 0 deletions additional_tests/exchanges_tests/test_bingx.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class TestBingxAuthenticatedExchange(
IGNORE_EXCHANGE_TRADE_ID = True
USE_ORDER_OPERATION_TO_CHECK_API_KEY_RIGHTS = True
EXPECT_MISSING_FEE_IN_CANCELLED_ORDERS = False
IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE = True # set True when is_authenticated_request is implemented

VALID_ORDER_ID = "1812980957928929280"

Expand All @@ -46,6 +47,9 @@ async def test_get_portfolio_with_market_filter(self):
async def test_get_account_id(self):
await super().test_get_account_id()

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
3 changes: 3 additions & 0 deletions additional_tests/exchanges_tests/test_bitget.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ async def test_get_account_id(self):
# pass if not implemented
pass

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
3 changes: 3 additions & 0 deletions additional_tests/exchanges_tests/test_bitmart.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ async def test_get_account_id(self):
# pass if not implemented
pass

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
3 changes: 3 additions & 0 deletions additional_tests/exchanges_tests/test_bybit.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ async def test_get_account_id(self):
# pass if not implemented
pass

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
3 changes: 3 additions & 0 deletions additional_tests/exchanges_tests/test_bybit_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ async def test_get_account_id(self):
# pass if not implemented
pass

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
4 changes: 4 additions & 0 deletions additional_tests/exchanges_tests/test_coinbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class TestCoinbaseAuthenticatedExchange(
USE_ORDER_OPERATION_TO_CHECK_API_KEY_RIGHTS = True # set True when api key rights can't be checked using a
EXPECT_MISSING_FEE_IN_CANCELLED_ORDERS = False
IS_BROKER_ENABLED_ACCOUNT = False
IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE = True # set True when is_authenticated_request is implemented

async def test_get_portfolio(self):
await super().test_get_portfolio()
Expand All @@ -45,6 +46,9 @@ async def test_get_portfolio_with_market_filter(self):
async def test_get_account_id(self):
await super().test_get_account_id()

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
3 changes: 3 additions & 0 deletions additional_tests/exchanges_tests/test_coinex.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ async def test_get_account_id(self):
# pass if not implemented
pass

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
3 changes: 3 additions & 0 deletions additional_tests/exchanges_tests/test_cryptocom.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ async def test_get_account_id(self):
# pass if not implemented
pass

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
3 changes: 3 additions & 0 deletions additional_tests/exchanges_tests/test_gateio.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ async def test_get_account_id(self):
# pass if not implemented
pass

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
# await super().test_invalid_api_key_error() # raises Request timeout
pass
Expand Down
3 changes: 3 additions & 0 deletions additional_tests/exchanges_tests/test_hollaex.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ async def test_get_portfolio_with_market_filter(self):
async def test_get_account_id(self):
await super().test_get_account_id()

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
3 changes: 3 additions & 0 deletions additional_tests/exchanges_tests/test_htx.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ async def test_get_account_id(self):
# pass if not implemented
pass

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
4 changes: 4 additions & 0 deletions additional_tests/exchanges_tests/test_kucoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class TestKucoinAuthenticatedExchange(
EXPECTED_GENERATED_ACCOUNT_ID = False # True when subaccounts are created
USE_ORDER_OPERATION_TO_CHECK_API_KEY_RIGHTS = True
VALID_ORDER_ID = "6617e84c5c1e0000083c71f7"
IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE = True # set True when is_authenticated_request is implemented

async def test_get_portfolio(self):
await super().test_get_portfolio()
Expand All @@ -50,6 +51,9 @@ async def test_create_and_cancel_limit_orders(self):
async def test_get_account_id(self):
await super().test_get_account_id()

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
4 changes: 4 additions & 0 deletions additional_tests/exchanges_tests/test_kucoin_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class TestKucoinFuturesAuthenticatedExchange(
USE_ORDER_OPERATION_TO_CHECK_API_KEY_RIGHTS = True
VALID_ORDER_ID = "6617e84c5c1e0000083c71f7"
EXPECT_MISSING_FEE_IN_CANCELLED_ORDERS = False
IS_AUTHENTICATED_REQUEST_CHECK_AVAILABLE = True # set True when is_authenticated_request is implemented

async def test_get_portfolio(self):
await super().test_get_portfolio()
Expand All @@ -48,6 +49,9 @@ async def test_get_portfolio_with_market_filter(self):
async def test_get_account_id(self):
await super().test_get_account_id()

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
3 changes: 3 additions & 0 deletions additional_tests/exchanges_tests/test_mexc.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ async def test_get_account_id(self):
# pass if not implemented
pass

async def test_is_authenticated_request(self):
await super().test_is_authenticated_request()

async def test_invalid_api_key_error(self):
await super().test_invalid_api_key_error()

Expand Down
Loading
Loading