From 4107e2ebab42f1af0d5e2d4edaf691f2f4c7bcce Mon Sep 17 00:00:00 2001 From: Nzubechukwu <103212159+Kaminar-i@users.noreply.github.com> Date: Sat, 2 Nov 2024 17:18:52 +0000 Subject: [PATCH 1/4] test: add test for _zklendLoanEntity --- .../loan_state/test_zklend_loan_entity.py | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 apps/data_handler/tests/loan_state/test_zklend_loan_entity.py diff --git a/apps/data_handler/tests/loan_state/test_zklend_loan_entity.py b/apps/data_handler/tests/loan_state/test_zklend_loan_entity.py new file mode 100644 index 00000000..ffcea610 --- /dev/null +++ b/apps/data_handler/tests/loan_state/test_zklend_loan_entity.py @@ -0,0 +1,178 @@ +import pytest +import decimal +from unittest.mock import MagicMock, patch +from shared.types import InterestRateModels, Portfolio, Prices, TokenParameters +from data_handler.handlers.loan_states.zklend.events import ZkLendLoanEntity +from shared.loan_entity import LoanEntity + +@pytest.fixture +def zklend_loan_entity(): + """Fixture providing a basic ZkLendLoanEntity instance with mocked methods.""" + with patch.object(LoanEntity, 'compute_collateral_usd') as mock_collateral_usd, \ + patch.object(LoanEntity, 'compute_debt_usd') as mock_debt_usd: + + # Setup default return values for the mocked methods + mock_collateral_usd.return_value = decimal.Decimal('2000') + mock_debt_usd.return_value = decimal.Decimal('1000') + + entity = ZkLendLoanEntity() + entity.compute_collateral_usd = mock_collateral_usd + entity.compute_debt_usd = mock_debt_usd + return entity + +@pytest.fixture +def mock_token_parameters(): + """Fixture providing mock token parameters.""" + eth_params = MagicMock() + eth_params.collateral_factor = decimal.Decimal('0.8') + eth_params.liquidation_bonus = decimal.Decimal('0.1') + eth_params.underlying_address = 'ETH' + + usdc_params = MagicMock() + usdc_params.collateral_factor = decimal.Decimal('0.85') + usdc_params.liquidation_bonus = decimal.Decimal('0.1') + usdc_params.underlying_address = 'USDC' + + return { + 'ETH': eth_params, + 'USDC': usdc_params + } + +@pytest.fixture +def mock_prices(): + """Fixture providing mock token prices.""" + return { + 'ETH': decimal.Decimal('2000'), + 'USDC': decimal.Decimal('1') + } + +@pytest.fixture +def mock_interest_rate_models(): + """Fixture providing mock interest rate models.""" + return { + 'ETH': decimal.Decimal('1.05'), + 'USDC': decimal.Decimal('1.02') + } + +class TestZkLendLoanEntity: + def test_initialization(self, zklend_loan_entity): + """Test proper initialization of ZkLendLoanEntity.""" + assert isinstance(zklend_loan_entity.deposit, Portfolio) + assert isinstance(zklend_loan_entity.collateral_enabled, dict) + assert len(zklend_loan_entity.deposit) == 0 + assert len(zklend_loan_entity.collateral_enabled) == 0 + + def test_compute_health_factor_no_debt(self, zklend_loan_entity, mock_prices): + """Test health factor computation when there's no debt.""" + zklend_loan_entity.collateral['ETH'] = decimal.Decimal('1.0') + zklend_loan_entity.compute_debt_usd.return_value = decimal.Decimal('0') + + health_factor = zklend_loan_entity.compute_health_factor( + standardized=False, + prices=mock_prices + ) + + assert health_factor == decimal.Decimal('Inf') + + def test_compute_health_factor_with_debt(self, zklend_loan_entity, mock_prices, mock_interest_rate_models): + """Test health factor computation with both collateral and debt.""" + zklend_loan_entity.collateral['ETH'] = decimal.Decimal('1.0') + zklend_loan_entity.debt['USDC'] = decimal.Decimal('1000.0') + + zklend_loan_entity.compute_collateral_usd.return_value = decimal.Decimal('2000') + zklend_loan_entity.compute_debt_usd.return_value = decimal.Decimal('1000') + + health_factor = zklend_loan_entity.compute_health_factor( + standardized=False, + collateral_interest_rate_models=mock_interest_rate_models, + debt_interest_rate_models=mock_interest_rate_models, + prices=mock_prices + ) + + assert float(health_factor) == pytest.approx(2.0) + + def test_compute_health_factor_with_risk_adjustment( + self, zklend_loan_entity, mock_prices, mock_token_parameters, mock_interest_rate_models + ): + """Test health factor computation with risk adjustment factors.""" + risk_adjusted_collateral_usd = decimal.Decimal('1600') # 2000 * 0.8 + debt_usd = decimal.Decimal('1000') + + health_factor = zklend_loan_entity.compute_health_factor( + standardized=True, + prices=mock_prices, + risk_adjusted_collateral_usd=risk_adjusted_collateral_usd, + debt_usd=debt_usd + ) + + assert float(health_factor) == pytest.approx(1.6) + + @pytest.mark.parametrize("collateral_amount,debt_amount,health_factor,expected_result", [ + (0, 1000, decimal.Decimal('0.5'), True), + (1, 3000, decimal.Decimal('0.8'), True), + (1, 1000, decimal.Decimal('2.0'), False), + (2, 1000, decimal.Decimal('4.0'), False), + ]) + def test_is_liquidatable( + self, + zklend_loan_entity, + mock_prices, + collateral_amount, + debt_amount, + health_factor, + expected_result + ): + """Test different scenarios for liquidation eligibility.""" + zklend_loan_entity.collateral['ETH'] = decimal.Decimal(str(collateral_amount)) + zklend_loan_entity.debt['USDC'] = decimal.Decimal(str(debt_amount)) + + # Setup the mock to return our predetermined health factor + collateral_usd = decimal.Decimal(str(collateral_amount)) * decimal.Decimal('2000') + debt_usd = decimal.Decimal(str(debt_amount)) + zklend_loan_entity.compute_collateral_usd.return_value = collateral_usd + zklend_loan_entity.compute_debt_usd.return_value = debt_usd + + actual_health_factor = zklend_loan_entity.compute_health_factor( + standardized=False, + prices=mock_prices + ) + + is_liquidatable = actual_health_factor < decimal.Decimal('1.0') + assert is_liquidatable == expected_result + + def test_negative_values(self, zklend_loan_entity): + """Test handling of negative values.""" + with patch.object(Portfolio, 'increase_value') as mock_increase: + mock_increase.side_effect = ValueError("Value cannot be negative") + with pytest.raises(ValueError): + zklend_loan_entity.collateral.increase_value('ETH', decimal.Decimal('-1.0')) + + with pytest.raises(ValueError): + zklend_loan_entity.debt.increase_value('USDC', decimal.Decimal('-1000.0')) + + def test_deposit_and_collateral_enabled_interaction(self, zklend_loan_entity): + """Test interaction between deposit and collateral_enabled flags.""" + token = 'ETH' + deposit_amount = decimal.Decimal('1.0') + + # Add deposit + zklend_loan_entity.deposit[token] = deposit_amount + assert zklend_loan_entity.deposit[token] == deposit_amount + + # Initially, collateral should not be enabled + assert not zklend_loan_entity.collateral_enabled.get(token, False) + assert token not in zklend_loan_entity.collateral + + # Enable collateral + zklend_loan_entity.collateral_enabled[token] = True + zklend_loan_entity.collateral[token] = deposit_amount + + assert zklend_loan_entity.collateral[token] == zklend_loan_entity.deposit[token] + + def test_token_settings(self): + """Test token settings configuration.""" + assert hasattr(ZkLendLoanEntity, 'TOKEN_SETTINGS') + assert isinstance(ZkLendLoanEntity.TOKEN_SETTINGS, dict) + +if __name__ == '__main__': + pytest.main(['-v']) \ No newline at end of file From 536b486a9be0123aa26218cd72d1a54579348f9c Mon Sep 17 00:00:00 2001 From: Nzubechukwu <103212159+Kaminar-i@users.noreply.github.com> Date: Sat, 2 Nov 2024 17:39:30 +0000 Subject: [PATCH 2/4] updates --- apps/data_handler/tests/loan_state/test_zklend_loan_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/data_handler/tests/loan_state/test_zklend_loan_entity.py b/apps/data_handler/tests/loan_state/test_zklend_loan_entity.py index ffcea610..224f555d 100644 --- a/apps/data_handler/tests/loan_state/test_zklend_loan_entity.py +++ b/apps/data_handler/tests/loan_state/test_zklend_loan_entity.py @@ -122,7 +122,7 @@ def test_is_liquidatable( health_factor, expected_result ): - """Test different scenarios for liquidation eligibility.""" + """Tests different scenarios for liquidation eligibility.""" zklend_loan_entity.collateral['ETH'] = decimal.Decimal(str(collateral_amount)) zklend_loan_entity.debt['USDC'] = decimal.Decimal(str(debt_amount)) From dd9d5804b4969cf216ad2bc8453ed76722292541 Mon Sep 17 00:00:00 2001 From: Nzubechukwu <103212159+Kaminar-i@users.noreply.github.com> Date: Sat, 2 Nov 2024 18:48:59 +0000 Subject: [PATCH 3/4] updates requested changes --- .../loan_state/test_zklend_loan_entity.py | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/apps/data_handler/tests/loan_state/test_zklend_loan_entity.py b/apps/data_handler/tests/loan_state/test_zklend_loan_entity.py index 224f555d..a4672651 100644 --- a/apps/data_handler/tests/loan_state/test_zklend_loan_entity.py +++ b/apps/data_handler/tests/loan_state/test_zklend_loan_entity.py @@ -1,3 +1,11 @@ +""" +Test module for the ZkLendLoanEntity class. + +This module contains test cases for verifying the functionality of the ZkLendLoanEntity class, +including health factor calculations, liquidation scenarios, and collateral management. +Tests cover normal operations, edge cases, and error conditions using mock objects and fixtures. +""" + import pytest import decimal from unittest.mock import MagicMock, patch @@ -12,8 +20,8 @@ def zklend_loan_entity(): patch.object(LoanEntity, 'compute_debt_usd') as mock_debt_usd: # Setup default return values for the mocked methods - mock_collateral_usd.return_value = decimal.Decimal('2000') - mock_debt_usd.return_value = decimal.Decimal('1000') + mock_collateral_usd.return_value = decimal.Decimal('2000') # 1 ETH at $2000 + mock_debt_usd.return_value = decimal.Decimal('1000') # 1000 USDC at $1 entity = ZkLendLoanEntity() entity.compute_collateral_usd = mock_collateral_usd @@ -55,6 +63,30 @@ def mock_interest_rate_models(): } class TestZkLendLoanEntity: + """ + Test suite for the ZkLendLoanEntity class. + + This class contains comprehensive tests for the ZkLendLoanEntity implementation, including: + - Basic initialization and property verification + - Health factor calculations for different scenarios: + * No debt cases + * With debt cases + * Risk-adjusted calculations + - Liquidation threshold testing: + * Healthy positions + * Underwater positions + - Collateral management: + * Deposit handling + * Collateral enabling/disabling + - Input validation: + * Negative value handling + * Invalid input testing + - Token configuration testing + + Tests use mock objects to isolate the class from external dependencies and + verify its behavior under various conditions. + """ + def test_initialization(self, zklend_loan_entity): """Test proper initialization of ZkLendLoanEntity.""" assert isinstance(zklend_loan_entity.deposit, Portfolio) @@ -108,10 +140,10 @@ def test_compute_health_factor_with_risk_adjustment( assert float(health_factor) == pytest.approx(1.6) @pytest.mark.parametrize("collateral_amount,debt_amount,health_factor,expected_result", [ - (0, 1000, decimal.Decimal('0.5'), True), - (1, 3000, decimal.Decimal('0.8'), True), - (1, 1000, decimal.Decimal('2.0'), False), - (2, 1000, decimal.Decimal('4.0'), False), + (0, 1000, decimal.Decimal('0.5'), True), # No collateral + (1, 3000, decimal.Decimal('0.8'), True), # Underwater position + (1, 1000, decimal.Decimal('2.0'), False), # Healthy position + (2, 1000, decimal.Decimal('4.0'), False), # Very healthy position ]) def test_is_liquidatable( self, @@ -122,7 +154,7 @@ def test_is_liquidatable( health_factor, expected_result ): - """Tests different scenarios for liquidation eligibility.""" + """Test different scenarios for liquidation eligibility.""" zklend_loan_entity.collateral['ETH'] = decimal.Decimal(str(collateral_amount)) zklend_loan_entity.debt['USDC'] = decimal.Decimal(str(debt_amount)) @@ -172,7 +204,4 @@ def test_deposit_and_collateral_enabled_interaction(self, zklend_loan_entity): def test_token_settings(self): """Test token settings configuration.""" assert hasattr(ZkLendLoanEntity, 'TOKEN_SETTINGS') - assert isinstance(ZkLendLoanEntity.TOKEN_SETTINGS, dict) - -if __name__ == '__main__': - pytest.main(['-v']) \ No newline at end of file + assert isinstance(ZkLendLoanEntity.TOKEN_SETTINGS, dict) \ No newline at end of file From fe4b7bf193b372817bdaa3e85b0b477cd6435ac7 Mon Sep 17 00:00:00 2001 From: Nzubechukwu <103212159+Kaminar-i@users.noreply.github.com> Date: Sat, 2 Nov 2024 19:09:07 +0000 Subject: [PATCH 4/4] updates --- .../versions/d3691f6e8c5a_add_zklend_events.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/data_handler/migrations/versions/d3691f6e8c5a_add_zklend_events.py b/apps/data_handler/migrations/versions/d3691f6e8c5a_add_zklend_events.py index f7b537a0..acda3e1f 100644 --- a/apps/data_handler/migrations/versions/d3691f6e8c5a_add_zklend_events.py +++ b/apps/data_handler/migrations/versions/d3691f6e8c5a_add_zklend_events.py @@ -4,6 +4,8 @@ Revises: 64a870953fa5 Create Date: 2024-11-01 10:53:33.024930 +This migration adds the necessary tables and relationships for tracking ZkLend events +in the system, including event tracking, transaction details and related metadata. """ from typing import Sequence, Union @@ -21,6 +23,14 @@ def upgrade() -> None: + """Upgrade the database schema to include ZkLend events. + + Creates new tables and relationships required for storing ZkLend event data, + including: + - Event tracking + - Transaction details + - Related metadata + """ # ### commands auto generated by Alembic - please adjust! ### op.create_table('accumulators_sync_event', sa.Column('token', sa.String(), nullable=False), @@ -116,6 +126,11 @@ def upgrade() -> None: def downgrade() -> None: + """Revert the database schema changes for ZkLend events. + + Removes tables and relationships that were added for ZkLend event tracking, + restoring the database to its previous state. + """ # ### commands auto generated by Alembic - please adjust! ### op.drop_index(op.f('ix_withdrawal_event_event_name'), table_name='withdrawal_event') op.drop_index(op.f('ix_withdrawal_event_block_number'), table_name='withdrawal_event') @@ -138,4 +153,4 @@ def downgrade() -> None: op.drop_index(op.f('ix_accumulators_sync_event_event_name'), table_name='accumulators_sync_event') op.drop_index(op.f('ix_accumulators_sync_event_block_number'), table_name='accumulators_sync_event') op.drop_table('accumulators_sync_event') - # ### end Alembic commands ### + # ### end Alembic commands ### \ No newline at end of file