From 465937b818c643fae88181c5d3627c7dc1c28ed9 Mon Sep 17 00:00:00 2001 From: NowGoody <96348811+NowGoody@users.noreply.github.com> Date: Thu, 28 Nov 2024 06:04:24 +0000 Subject: [PATCH 1/4] feat: Create test cases for protocol stats --- apps/dashboard_app/tests/__init__.py | 0 .../tests/protocol_stats_test.py | 89 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 apps/dashboard_app/tests/__init__.py create mode 100644 apps/dashboard_app/tests/protocol_stats_test.py diff --git a/apps/dashboard_app/tests/__init__.py b/apps/dashboard_app/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/dashboard_app/tests/protocol_stats_test.py b/apps/dashboard_app/tests/protocol_stats_test.py new file mode 100644 index 00000000..81129aaf --- /dev/null +++ b/apps/dashboard_app/tests/protocol_stats_test.py @@ -0,0 +1,89 @@ +import pytest +import pandas as pd +from decimal import Decimal +from unittest.mock import MagicMock, patch +from dashboard_app.helpers.tools import get_addresses, get_underlying_address +from dashboard_app.helpers.loans_table import get_protocol, get_supply_function_call_parameters +from data_handler.handlers import blockchain_call +from shared.types import Prices +from shared.constants import TOKEN_SETTINGS +from shared.state import State +from dashboard_app.helpers.protocol_stats import ( + get_general_stats, + get_supply_stats, + get_collateral_stats, + get_debt_stats, + get_utilization_stats, +) +from data_handler.handlers.loan_states.zklend.events import ZkLendState + + +@pytest.fixture +def mock_zklend_state(): + """Fixture to mock ZkLendState and its methods.""" + mock_state = MagicMock(spec=ZkLendState) + mock_state.loan_entities = { + "user1": MagicMock(), + "user2": MagicMock(), + } + mock_state.compute_number_of_active_loan_entities.return_value = 2 + mock_state.compute_number_of_active_loan_entities_with_debt.return_value = 1 + mock_state.token_parameters = MagicMock() + mock_state.interest_rate_models = MagicMock() + return mock_state + + +@pytest.fixture +def mock_states(): + """Fixture for mock states with relevant methods.""" + state1 = MagicMock(spec=State) + state1.loan_entities = {"user1": MagicMock(), "user2": MagicMock()} + state1.compute_number_of_active_loan_entities.return_value = 2 + state1.compute_number_of_active_loan_entities_with_debt.return_value = 1 + state1.token_parameters = MagicMock() + state1.interest_rate_models = MagicMock() + + state2 = MagicMock(spec=State) + state2.loan_entities = {"user3": MagicMock()} + state2.compute_number_of_active_loan_entities.return_value = 1 + state2.compute_number_of_active_loan_entities_with_debt.return_value = 1 + state2.token_parameters = MagicMock() + state2.interest_rate_models = MagicMock() + + return [state1, state2] + + +def test_get_general_stats(mock_zklend_state): + """Test get_general_stats with mocked ZkLendState.""" + loan_stats = { + "zkLend": pd.DataFrame( + { + "Debt (USD)": [500, 700], + "Risk-adjusted collateral (USD)": [1000, 1200], + "Collateral (USD)": [1200, 1400], + } + ) + } + + with patch("dashboard_app.helpers.loans_table.get_protocol", return_value="zkLend"): + df = get_general_stats([mock_zklend_state], loan_stats) + assert "Protocol" in df.columns + assert df.loc[0, "Protocol"] == "zkLend" + assert df.loc[0, "Total debt (USD)"] == 1200 # Sum of loan_stats['Debt (USD)'] + + +def test_get_collateral_stats(mock_zklend_state): + """Test get_collateral_stats with mocked ZkLendState.""" + with patch("dashboard_app.helpers.loans_table.get_protocol", return_value="zkLend"): + df = get_collateral_stats([mock_zklend_state]) + assert "Protocol" in df.columns + assert "ETH collateral" in df.columns + + +def test_get_debt_stats(mock_zklend_state): + """Test get_debt_stats with mocked ZkLendState.""" + with patch("dashboard_app.helpers.loans_table.get_protocol", return_value="zkLend"): + df = get_debt_stats([mock_zklend_state]) + assert "Protocol" in df.columns + assert "ETH debt" in df.columns + From 72a7da0e24e8d6b659467ca645912c830d078653 Mon Sep 17 00:00:00 2001 From: NowGoody <96348811+NowGoody@users.noreply.github.com> Date: Fri, 29 Nov 2024 08:22:09 +0000 Subject: [PATCH 2/4] complete protocol_stats test --- apps/dashboard_app/helpers/protocol_stats.py | 4 +- .../tests/protocol_stats_test.py | 241 +++++++++++++----- 2 files changed, 180 insertions(+), 65 deletions(-) diff --git a/apps/dashboard_app/helpers/protocol_stats.py b/apps/dashboard_app/helpers/protocol_stats.py index a1ab6d70..4f30322e 100644 --- a/apps/dashboard_app/helpers/protocol_stats.py +++ b/apps/dashboard_app/helpers/protocol_stats.py @@ -253,5 +253,7 @@ def get_utilization_stats( }, ) utilization_columns = [x for x in data.columns if "utilization" in x] - data[utilization_columns] = data[utilization_columns].map(lambda x: round(x, 4)) + # NOTE: This was made so test can pass, something to look into + data[utilization_columns] = data[utilization_columns].apply(lambda x: x.round(4)) + # data[utilization_columns] = data[utilization_columns].map(lambda x: round(x, 4)) return data diff --git a/apps/dashboard_app/tests/protocol_stats_test.py b/apps/dashboard_app/tests/protocol_stats_test.py index 81129aaf..ce0eeb76 100644 --- a/apps/dashboard_app/tests/protocol_stats_test.py +++ b/apps/dashboard_app/tests/protocol_stats_test.py @@ -1,13 +1,10 @@ -import pytest import pandas as pd +import pytest from decimal import Decimal from unittest.mock import MagicMock, patch -from dashboard_app.helpers.tools import get_addresses, get_underlying_address -from dashboard_app.helpers.loans_table import get_protocol, get_supply_function_call_parameters -from data_handler.handlers import blockchain_call -from shared.types import Prices -from shared.constants import TOKEN_SETTINGS + from shared.state import State +from shared.types import Prices from dashboard_app.helpers.protocol_stats import ( get_general_stats, get_supply_stats, @@ -15,75 +12,191 @@ get_debt_stats, get_utilization_stats, ) -from data_handler.handlers.loan_states.zklend.events import ZkLendState @pytest.fixture -def mock_zklend_state(): - """Fixture to mock ZkLendState and its methods.""" - mock_state = MagicMock(spec=ZkLendState) - mock_state.loan_entities = { - "user1": MagicMock(), - "user2": MagicMock(), +def token_addresses(): + return { + "ETH": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + "WBTC": "0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac", + "wBTC": "0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac", + "USDC": "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8", + "DAI": "0x00da114221cb83fa859dbdb4c44beeaa0bb37c7537ad5ae66fe5e0efd20e6eb3", + "USDT": "0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8", + "wstETH": "0x042b8f0484674ca266ac5d08e4ac6a3fe65bd3129795def2dca5c34ecc5f96d2", + "LORDS": "0x0124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49", + "STRK": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d" + } + + +@pytest.fixture +def mock_loan_stats(): + return { + "zkLend": pd.DataFrame({ + "Debt (USD)": [1000], + "Risk-adjusted collateral (USD)": [2000], + "Collateral (USD)": [2500], + }) + } + + +@pytest.fixture +def mock_state(token_addresses): + state = MagicMock(spec=State) + state.get_protocol_name.return_value = "zkLend" + state.PROTOCOL_NAME = "zkLend" + state.compute_number_of_active_loan_entities.return_value = 10 + state.compute_number_of_active_loan_entities_with_debt.return_value = 5 + + # Setup loan entity with default values + loan_entity = MagicMock() + loan_entity.collateral.values = {addr: "0" for addr in token_addresses.values()} + loan_entity.debt = {addr: "0" for addr in token_addresses.values()} + loan_entity.collateral.values[token_addresses["ETH"]] = "1000000000000000000" + loan_entity.debt[token_addresses["ETH"]] = "2000000000000000000" + state.loan_entities = {"user1": loan_entity} + + # Setup token parameters + class TokenParam: + def __init__(self, address, symbol): + self.address = address + self.underlying_symbol = symbol + self.underlying_address = address + self.decimal_factor = 1e18 + + token_params = { + symbol: TokenParam(addr, symbol) + for symbol, addr in token_addresses.items() + } + + state.token_parameters = MagicMock() + state.token_parameters.collateral = token_params + state.token_parameters.debt = token_params.copy() + + # Setup interest rate models + interest_rates = {addr: "1.0" for addr in token_addresses.values()} + state.interest_rate_models = MagicMock() + state.interest_rate_models.collateral = interest_rates + state.interest_rate_models.debt = interest_rates.copy() + + with patch("dashboard_app.helpers.protocol_stats.get_protocol", return_value="zkLend"): + yield state + + +@pytest.fixture +def mock_prices(token_addresses): + return { + token_addresses["ETH"]: "2000", + token_addresses["WBTC"]: "30000", + token_addresses["USDC"]: "1", + token_addresses["DAI"]: "1", + token_addresses["USDT"]: "1", + token_addresses["wstETH"]: "2100", + token_addresses["LORDS"]: "0.5", + token_addresses["STRK"]: "1.2", } - mock_state.compute_number_of_active_loan_entities.return_value = 2 - mock_state.compute_number_of_active_loan_entities_with_debt.return_value = 1 - mock_state.token_parameters = MagicMock() - mock_state.interest_rate_models = MagicMock() - return mock_state @pytest.fixture -def mock_states(): - """Fixture for mock states with relevant methods.""" - state1 = MagicMock(spec=State) - state1.loan_entities = {"user1": MagicMock(), "user2": MagicMock()} - state1.compute_number_of_active_loan_entities.return_value = 2 - state1.compute_number_of_active_loan_entities_with_debt.return_value = 1 - state1.token_parameters = MagicMock() - state1.interest_rate_models = MagicMock() - - state2 = MagicMock(spec=State) - state2.loan_entities = {"user3": MagicMock()} - state2.compute_number_of_active_loan_entities.return_value = 1 - state2.compute_number_of_active_loan_entities_with_debt.return_value = 1 - state2.token_parameters = MagicMock() - state2.interest_rate_models = MagicMock() - - return [state1, state2] - - -def test_get_general_stats(mock_zklend_state): - """Test get_general_stats with mocked ZkLendState.""" - loan_stats = { - "zkLend": pd.DataFrame( - { - "Debt (USD)": [500, 700], - "Risk-adjusted collateral (USD)": [1000, 1200], - "Collateral (USD)": [1200, 1400], - } - ) +def mock_token_settings(token_addresses): + return { + symbol: MagicMock( + decimal_factor=Decimal("1e18"), + address=addr + ) for symbol, addr in token_addresses.items() } - with patch("dashboard_app.helpers.loans_table.get_protocol", return_value="zkLend"): - df = get_general_stats([mock_zklend_state], loan_stats) - assert "Protocol" in df.columns - assert df.loc[0, "Protocol"] == "zkLend" - assert df.loc[0, "Total debt (USD)"] == 1200 # Sum of loan_stats['Debt (USD)'] + +@pytest.fixture(autouse=True) +def patch_token_settings(mock_token_settings): + with patch("dashboard_app.helpers.protocol_stats.TOKEN_SETTINGS", mock_token_settings): + yield -def test_get_collateral_stats(mock_zklend_state): - """Test get_collateral_stats with mocked ZkLendState.""" - with patch("dashboard_app.helpers.loans_table.get_protocol", return_value="zkLend"): - df = get_collateral_stats([mock_zklend_state]) - assert "Protocol" in df.columns - assert "ETH collateral" in df.columns +def test_get_general_stats(mock_state, mock_loan_stats): + result = get_general_stats([mock_state], mock_loan_stats) + + assert isinstance(result, pd.DataFrame) + assert "Protocol" in result.columns + assert result["Number of active users"].iloc[0] == 10 + assert result["Number of active borrowers"].iloc[0] == 5 + assert result["Total debt (USD)"].iloc[0] == 1000 -def test_get_debt_stats(mock_zklend_state): - """Test get_debt_stats with mocked ZkLendState.""" - with patch("dashboard_app.helpers.loans_table.get_protocol", return_value="zkLend"): - df = get_debt_stats([mock_zklend_state]) - assert "Protocol" in df.columns - assert "ETH debt" in df.columns +@patch("dashboard_app.helpers.protocol_stats.get_protocol") +@patch("dashboard_app.helpers.protocol_stats.get_supply_function_call_parameters") +@patch("dashboard_app.helpers.protocol_stats.asyncio.run") +def test_get_supply_stats( + mock_run, + mock_get_params, + mock_get_protocol, + mock_state, + mock_prices, + token_addresses, + mock_token_settings +): + mock_get_protocol.return_value = "zkLend" + mock_get_params.return_value = ([token_addresses["ETH"]], "felt_total_supply") + mock_run.return_value = [Decimal("1000000000000000000")] + + # Convert prices to Decimal + prices = {k: Decimal(v) for k, v in mock_prices.items()} + + result = get_supply_stats([mock_state], prices) + + assert isinstance(result, pd.DataFrame) + assert "Protocol" in result.columns + assert "ETH supply" in result.columns + + +@patch("dashboard_app.helpers.protocol_stats.get_protocol") +def test_get_collateral_stats(mock_get_protocol, mock_state, token_addresses): + mock_get_protocol.return_value = "zkLend" + + result = get_collateral_stats([mock_state]) + + assert isinstance(result, pd.DataFrame) + assert "Protocol" in result.columns + assert "ETH collateral" in result.columns + +@patch("dashboard_app.helpers.protocol_stats.get_protocol") +def test_get_debt_stats(mock_get_protocol, mock_state, token_addresses): + mock_get_protocol.return_value = "zkLend" + + result = get_debt_stats([mock_state]) + + assert isinstance(result, pd.DataFrame) + assert "Protocol" in result.columns + assert "ETH debt" in result.columns + + +def test_get_utilization_stats(): + general_stats = pd.DataFrame({ + "Protocol": ["zkLend"], + "Total debt (USD)": [1000], + }) + + supply_stats = pd.DataFrame({ + "Protocol": ["zkLend"], + "Total supply (USD)": [4000], + **{f"{token} supply": [1000 if token in ["USDC", "DAI", "USDT", "LORDS", "STRK"] else 1] + for token in ["ETH", "WBTC", "USDC", "DAI", "USDT", "wstETH", "LORDS", "STRK"]} + }) + + debt_stats = pd.DataFrame({ + "Protocol": ["zkLend"], + **{f"{token} debt": [500 if token in ["USDC", "DAI", "USDT", "LORDS", "STRK"] else 0.5] + for token in ["ETH", "WBTC", "USDC", "DAI", "USDT", "wstETH", "LORDS", "STRK"]} + }) + + result = get_utilization_stats(general_stats, supply_stats, debt_stats) + + # Round results for comparison + utilization_columns = [col for col in result.columns if col != "Protocol"] + result[utilization_columns] = result[utilization_columns].applymap(lambda x: round(x, 4)) + + assert isinstance(result, pd.DataFrame) + assert "Protocol" in result.columns + assert "Total utilization" in result.columns + assert "ETH utilization" in result.columns From 10dbabc5968544921608a924341af5db01517123 Mon Sep 17 00:00:00 2001 From: NowGoody <96348811+NowGoody@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:47:18 +0000 Subject: [PATCH 3/4] test: add negative test cases --- apps/dashboard_app/helpers/protocol_stats.py | 2 +- .../tests/protocol_stats_test.py | 70 ++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/apps/dashboard_app/helpers/protocol_stats.py b/apps/dashboard_app/helpers/protocol_stats.py index 4f30322e..4cc74009 100644 --- a/apps/dashboard_app/helpers/protocol_stats.py +++ b/apps/dashboard_app/helpers/protocol_stats.py @@ -253,7 +253,7 @@ def get_utilization_stats( }, ) utilization_columns = [x for x in data.columns if "utilization" in x] - # NOTE: This was made so test can pass, something to look into + # NOTE: .map doesn't work for some reason. apply works data[utilization_columns] = data[utilization_columns].apply(lambda x: x.round(4)) # data[utilization_columns] = data[utilization_columns].map(lambda x: round(x, 4)) return data diff --git a/apps/dashboard_app/tests/protocol_stats_test.py b/apps/dashboard_app/tests/protocol_stats_test.py index ce0eeb76..3c20127f 100644 --- a/apps/dashboard_app/tests/protocol_stats_test.py +++ b/apps/dashboard_app/tests/protocol_stats_test.py @@ -123,6 +123,17 @@ def test_get_general_stats(mock_state, mock_loan_stats): assert result["Total debt (USD)"].iloc[0] == 1000 +def test_get_general_stats_empty_state(): + result = get_general_stats([], {}) + assert isinstance(result, pd.DataFrame) + assert len(result) == 0 + + +def test_get_general_stats_invalid_loan_stats(mock_state): + with pytest.raises(KeyError): + get_general_stats([mock_state], {"InvalidProtocol": pd.DataFrame()}) + + @patch("dashboard_app.helpers.protocol_stats.get_protocol") @patch("dashboard_app.helpers.protocol_stats.get_supply_function_call_parameters") @patch("dashboard_app.helpers.protocol_stats.asyncio.run") @@ -149,6 +160,25 @@ def test_get_supply_stats( assert "ETH supply" in result.columns +@patch("dashboard_app.helpers.protocol_stats.get_protocol") +@patch("dashboard_app.helpers.protocol_stats.get_supply_function_call_parameters") +@patch("dashboard_app.helpers.protocol_stats.asyncio.run") +def test_get_supply_stats_blockchain_error( + mock_run, + mock_get_params, + mock_get_protocol, + mock_state, + mock_prices, + token_addresses +): + mock_get_protocol.return_value = "zkLend" + mock_get_params.return_value = ([token_addresses["ETH"]], "felt_total_supply") + mock_run.side_effect = Exception("Blockchain call failed") + + with pytest.raises(Exception): + get_supply_stats([mock_state], mock_prices) + + @patch("dashboard_app.helpers.protocol_stats.get_protocol") def test_get_collateral_stats(mock_get_protocol, mock_state, token_addresses): mock_get_protocol.return_value = "zkLend" @@ -160,6 +190,13 @@ def test_get_collateral_stats(mock_get_protocol, mock_state, token_addresses): assert "ETH collateral" in result.columns +def test_get_collateral_stats_invalid_protocol(mock_state): + with patch("dashboard_app.helpers.protocol_stats.get_protocol") as mock_get_protocol: + mock_get_protocol.return_value = "InvalidProtocol" + with pytest.raises(ValueError): + get_collateral_stats([mock_state]) + + @patch("dashboard_app.helpers.protocol_stats.get_protocol") def test_get_debt_stats(mock_get_protocol, mock_state, token_addresses): mock_get_protocol.return_value = "zkLend" @@ -171,6 +208,13 @@ def test_get_debt_stats(mock_get_protocol, mock_state, token_addresses): assert "ETH debt" in result.columns +def test_get_debt_stats_invalid_protocol(mock_state): + with patch("dashboard_app.helpers.protocol_stats.get_protocol") as mock_get_protocol: + mock_get_protocol.return_value = "InvalidProtocol" + with pytest.raises(ValueError): + get_debt_stats([mock_state]) + + def test_get_utilization_stats(): general_stats = pd.DataFrame({ "Protocol": ["zkLend"], @@ -192,7 +236,6 @@ def test_get_utilization_stats(): result = get_utilization_stats(general_stats, supply_stats, debt_stats) - # Round results for comparison utilization_columns = [col for col in result.columns if col != "Protocol"] result[utilization_columns] = result[utilization_columns].applymap(lambda x: round(x, 4)) @@ -200,3 +243,28 @@ def test_get_utilization_stats(): assert "Protocol" in result.columns assert "Total utilization" in result.columns assert "ETH utilization" in result.columns + + +def test_get_utilization_stats_division_by_zero(): + general_stats = pd.DataFrame({ + "Protocol": ["zkLend"], + "Total debt (USD)": [1000], + }) + + supply_stats = pd.DataFrame({ + "Protocol": ["zkLend"], + "Total supply (USD)": [0], + **{f"{token} supply": [0] for token in ["ETH", "WBTC", "USDC", "DAI", "USDT", "wstETH", "LORDS", "STRK"]} + }) + + debt_stats = pd.DataFrame({ + "Protocol": ["zkLend"], + **{f"{token} debt": [0] for token in ["ETH", "WBTC", "USDC", "DAI", "USDT", "wstETH", "LORDS", "STRK"]} + }) + + result = get_utilization_stats(general_stats, supply_stats, debt_stats) + + # Check if division by zero results in NaN or infinity + assert result["Total utilization"].iloc[0] == 1 # When supply is 0, utilization should be 100% + assert pd.isna(result["ETH utilization"].iloc[0]) # When both supply and debt are 0, result should be NaN + \ No newline at end of file From 184096b547fc5542ef080465bf672060b70d9edf Mon Sep 17 00:00:00 2001 From: NowGoody <96348811+NowGoody@users.noreply.github.com> Date: Sun, 1 Dec 2024 06:37:41 +0000 Subject: [PATCH 4/4] fix: pylint fix --- .../tests/protocol_stats_test.py | 167 +++++++++++++----- 1 file changed, 123 insertions(+), 44 deletions(-) diff --git a/apps/dashboard_app/tests/protocol_stats_test.py b/apps/dashboard_app/tests/protocol_stats_test.py index 3c20127f..bda9a358 100644 --- a/apps/dashboard_app/tests/protocol_stats_test.py +++ b/apps/dashboard_app/tests/protocol_stats_test.py @@ -1,10 +1,13 @@ -import pandas as pd -import pytest +""" +Tests for the protocol_stats module. +""" + from decimal import Decimal from unittest.mock import MagicMock, patch +import pandas as pd +import pytest -from shared.state import State -from shared.types import Prices +# from shared.types import Prices from dashboard_app.helpers.protocol_stats import ( get_general_stats, get_supply_stats, @@ -12,10 +15,14 @@ get_debt_stats, get_utilization_stats, ) +from shared.state import State @pytest.fixture def token_addresses(): + """ + Returns a dictionary of token addresses. + """ return { "ETH": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "WBTC": "0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac", @@ -31,6 +38,9 @@ def token_addresses(): @pytest.fixture def mock_loan_stats(): + """ + Returns a dictionary of loan stats. + """ return { "zkLend": pd.DataFrame({ "Debt (USD)": [1000], @@ -42,12 +52,15 @@ def mock_loan_stats(): @pytest.fixture def mock_state(token_addresses): + """ + Returns a mock state object. + """ state = MagicMock(spec=State) state.get_protocol_name.return_value = "zkLend" state.PROTOCOL_NAME = "zkLend" state.compute_number_of_active_loan_entities.return_value = 10 state.compute_number_of_active_loan_entities_with_debt.return_value = 5 - + # Setup loan entity with default values loan_entity = MagicMock() loan_entity.collateral.values = {addr: "0" for addr in token_addresses.values()} @@ -55,36 +68,45 @@ def mock_state(token_addresses): loan_entity.collateral.values[token_addresses["ETH"]] = "1000000000000000000" loan_entity.debt[token_addresses["ETH"]] = "2000000000000000000" state.loan_entities = {"user1": loan_entity} - + # Setup token parameters class TokenParam: + """ + A class to represent token parameters. + """ def __init__(self, address, symbol): self.address = address self.underlying_symbol = symbol self.underlying_address = address self.decimal_factor = 1e18 - + token_params = { symbol: TokenParam(addr, symbol) for symbol, addr in token_addresses.items() } - + state.token_parameters = MagicMock() state.token_parameters.collateral = token_params state.token_parameters.debt = token_params.copy() - + # Setup interest rate models interest_rates = {addr: "1.0" for addr in token_addresses.values()} state.interest_rate_models = MagicMock() state.interest_rate_models.collateral = interest_rates state.interest_rate_models.debt = interest_rates.copy() - - with patch("dashboard_app.helpers.protocol_stats.get_protocol", return_value="zkLend"): + + with patch( + "dashboard_app.helpers.protocol_stats.get_protocol", + return_value="zkLend" + ): yield state @pytest.fixture def mock_prices(token_addresses): + """ + Returns a dictionary of prices. + """ return { token_addresses["ETH"]: "2000", token_addresses["WBTC"]: "30000", @@ -99,6 +121,9 @@ def mock_prices(token_addresses): @pytest.fixture def mock_token_settings(token_addresses): + """ + Returns a dictionary of token settings. + """ return { symbol: MagicMock( decimal_factor=Decimal("1e18"), @@ -109,13 +134,22 @@ def mock_token_settings(token_addresses): @pytest.fixture(autouse=True) def patch_token_settings(mock_token_settings): - with patch("dashboard_app.helpers.protocol_stats.TOKEN_SETTINGS", mock_token_settings): + """ + Patches the TOKEN_SETTINGS dictionary. + """ + with patch( + "dashboard_app.helpers.protocol_stats.TOKEN_SETTINGS", + mock_token_settings + ): yield def test_get_general_stats(mock_state, mock_loan_stats): + """ + Tests the get_general_stats function. + """ result = get_general_stats([mock_state], mock_loan_stats) - + assert isinstance(result, pd.DataFrame) assert "Protocol" in result.columns assert result["Number of active users"].iloc[0] == 10 @@ -124,12 +158,18 @@ def test_get_general_stats(mock_state, mock_loan_stats): def test_get_general_stats_empty_state(): + """ + Tests the get_general_stats function with an empty state. + """ result = get_general_stats([], {}) assert isinstance(result, pd.DataFrame) assert len(result) == 0 def test_get_general_stats_invalid_loan_stats(mock_state): + """ + Tests the get_general_stats function with an invalid loan stats. + """ with pytest.raises(KeyError): get_general_stats([mock_state], {"InvalidProtocol": pd.DataFrame()}) @@ -146,15 +186,18 @@ def test_get_supply_stats( token_addresses, mock_token_settings ): + """ + Tests the get_supply_stats function. + """ mock_get_protocol.return_value = "zkLend" mock_get_params.return_value = ([token_addresses["ETH"]], "felt_total_supply") mock_run.return_value = [Decimal("1000000000000000000")] - + # Convert prices to Decimal prices = {k: Decimal(v) for k, v in mock_prices.items()} - + result = get_supply_stats([mock_state], prices) - + assert isinstance(result, pd.DataFrame) assert "Protocol" in result.columns assert "ETH supply" in result.columns @@ -164,34 +207,45 @@ def test_get_supply_stats( @patch("dashboard_app.helpers.protocol_stats.get_supply_function_call_parameters") @patch("dashboard_app.helpers.protocol_stats.asyncio.run") def test_get_supply_stats_blockchain_error( - mock_run, - mock_get_params, + mock_run, + mock_get_params, mock_get_protocol, mock_state, mock_prices, token_addresses ): + """ + Tests the get_supply_stats function with a blockchain error. + """ mock_get_protocol.return_value = "zkLend" mock_get_params.return_value = ([token_addresses["ETH"]], "felt_total_supply") mock_run.side_effect = Exception("Blockchain call failed") - + with pytest.raises(Exception): get_supply_stats([mock_state], mock_prices) @patch("dashboard_app.helpers.protocol_stats.get_protocol") def test_get_collateral_stats(mock_get_protocol, mock_state, token_addresses): + """ + Tests the get_collateral_stats function. + """ mock_get_protocol.return_value = "zkLend" - + result = get_collateral_stats([mock_state]) - + assert isinstance(result, pd.DataFrame) assert "Protocol" in result.columns assert "ETH collateral" in result.columns def test_get_collateral_stats_invalid_protocol(mock_state): - with patch("dashboard_app.helpers.protocol_stats.get_protocol") as mock_get_protocol: + """ + Tests the get_collateral_stats function with an invalid protocol. + """ + with patch( + "dashboard_app.helpers.protocol_stats.get_protocol" + ) as mock_get_protocol: mock_get_protocol.return_value = "InvalidProtocol" with pytest.raises(ValueError): get_collateral_stats([mock_state]) @@ -199,46 +253,65 @@ def test_get_collateral_stats_invalid_protocol(mock_state): @patch("dashboard_app.helpers.protocol_stats.get_protocol") def test_get_debt_stats(mock_get_protocol, mock_state, token_addresses): + """ + Tests the get_debt_stats function. + """ mock_get_protocol.return_value = "zkLend" - + result = get_debt_stats([mock_state]) - + assert isinstance(result, pd.DataFrame) assert "Protocol" in result.columns assert "ETH debt" in result.columns def test_get_debt_stats_invalid_protocol(mock_state): - with patch("dashboard_app.helpers.protocol_stats.get_protocol") as mock_get_protocol: + """ + Tests the get_debt_stats function with an invalid protocol. + """ + with patch( + "dashboard_app.helpers.protocol_stats.get_protocol" + ) as mock_get_protocol: mock_get_protocol.return_value = "InvalidProtocol" with pytest.raises(ValueError): get_debt_stats([mock_state]) def test_get_utilization_stats(): + """ + Tests the get_utilization_stats function. + """ general_stats = pd.DataFrame({ "Protocol": ["zkLend"], "Total debt (USD)": [1000], }) - + supply_stats = pd.DataFrame({ "Protocol": ["zkLend"], "Total supply (USD)": [4000], - **{f"{token} supply": [1000 if token in ["USDC", "DAI", "USDT", "LORDS", "STRK"] else 1] - for token in ["ETH", "WBTC", "USDC", "DAI", "USDT", "wstETH", "LORDS", "STRK"]} + **{f"{token} supply": [ + 1000 if token in ["USDC", "DAI", "USDT", "LORDS", "STRK"] else 1 + ] for token in [ + "ETH", "WBTC", "USDC", "DAI", "USDT", "wstETH", "LORDS", "STRK" + ]} }) - + debt_stats = pd.DataFrame({ "Protocol": ["zkLend"], - **{f"{token} debt": [500 if token in ["USDC", "DAI", "USDT", "LORDS", "STRK"] else 0.5] - for token in ["ETH", "WBTC", "USDC", "DAI", "USDT", "wstETH", "LORDS", "STRK"]} + **{f"{token} debt": [ + 500 if token in ["USDC", "DAI", "USDT", "LORDS", "STRK"] else 0.5 + ] for token in [ + "ETH", "WBTC", "USDC", "DAI", "USDT", "wstETH", "LORDS", "STRK" + ]} }) - + result = get_utilization_stats(general_stats, supply_stats, debt_stats) - + utilization_columns = [col for col in result.columns if col != "Protocol"] - result[utilization_columns] = result[utilization_columns].applymap(lambda x: round(x, 4)) - + result[utilization_columns] = result[utilization_columns].applymap( + lambda x: round(x, 4) + ) + assert isinstance(result, pd.DataFrame) assert "Protocol" in result.columns assert "Total utilization" in result.columns @@ -246,25 +319,31 @@ def test_get_utilization_stats(): def test_get_utilization_stats_division_by_zero(): + """ + Tests the get_utilization_stats function with a division by zero. + """ general_stats = pd.DataFrame({ "Protocol": ["zkLend"], "Total debt (USD)": [1000], }) - + supply_stats = pd.DataFrame({ "Protocol": ["zkLend"], "Total supply (USD)": [0], - **{f"{token} supply": [0] for token in ["ETH", "WBTC", "USDC", "DAI", "USDT", "wstETH", "LORDS", "STRK"]} + **{f"{token} supply": [0] for token in [ + "ETH", "WBTC", "USDC", "DAI", "USDT", "wstETH", "LORDS", "STRK" + ]} }) - + debt_stats = pd.DataFrame({ "Protocol": ["zkLend"], - **{f"{token} debt": [0] for token in ["ETH", "WBTC", "USDC", "DAI", "USDT", "wstETH", "LORDS", "STRK"]} + **{f"{token} debt": [0] for token in [ + "ETH", "WBTC", "USDC", "DAI", "USDT", "wstETH", "LORDS", "STRK" + ]} }) - + result = get_utilization_stats(general_stats, supply_stats, debt_stats) - + # Check if division by zero results in NaN or infinity - assert result["Total utilization"].iloc[0] == 1 # When supply is 0, utilization should be 100% - assert pd.isna(result["ETH utilization"].iloc[0]) # When both supply and debt are 0, result should be NaN - \ No newline at end of file + assert result["Total utilization"].iloc[0] == 1 + assert pd.isna(result["ETH utilization"].iloc[0])