Skip to content

Commit b482786

Browse files
test: added unit testing for fare.py
1 parent 958f236 commit b482786

1 file changed

Lines changed: 380 additions & 0 deletions

File tree

tests/unit/test_fare.py

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
"""
2+
Unit tests for virtuals_acp.fare module
3+
"""
4+
5+
import pytest
6+
from decimal import Decimal
7+
from unittest.mock import MagicMock, patch
8+
9+
from virtuals_acp.fare import Fare, FareAmount, FareBigInt, FareAmountBase, WETH_FARE, ETH_FARE
10+
from virtuals_acp.exceptions import ACPError
11+
12+
13+
class TestFare:
14+
"""Test suite for Fare class"""
15+
16+
class TestInitialization:
17+
"""Test Fare initialization"""
18+
19+
def test_should_initialize_with_contract_address_and_decimals(self):
20+
"""Should initialize with contract address and decimals"""
21+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
22+
23+
assert fare.contract_address == "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
24+
assert fare.decimals == 6
25+
26+
def test_should_convert_address_to_checksum(self):
27+
"""Should convert contract address to checksum format"""
28+
# Lowercase address
29+
fare = Fare("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", 6)
30+
31+
# Should be checksummed
32+
assert fare.contract_address == "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
33+
34+
def test_should_handle_18_decimals(self):
35+
"""Should handle 18 decimals for ETH-like tokens"""
36+
fare = Fare("0x4200000000000000000000000000000000000006", 18)
37+
38+
assert fare.decimals == 18
39+
40+
class TestFormatAmount:
41+
"""Test format_amount method"""
42+
43+
def test_should_format_integer_amount(self):
44+
"""Should format integer amount to smallest unit"""
45+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
46+
47+
result = fare.format_amount(100)
48+
49+
assert result == 100000000 # 100 * 10^6
50+
51+
def test_should_format_float_amount(self):
52+
"""Should format float amount to smallest unit"""
53+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
54+
55+
result = fare.format_amount(1.5)
56+
57+
assert result == 1500000 # 1.5 * 10^6
58+
59+
def test_should_format_decimal_amount(self):
60+
"""Should format Decimal amount to smallest unit"""
61+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
62+
63+
result = fare.format_amount(Decimal("2.5"))
64+
65+
assert result == 2500000 # 2.5 * 10^6
66+
67+
def test_should_round_down_fractional_smallest_unit(self):
68+
"""Should round down when converting to smallest unit"""
69+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
70+
71+
# 1.5555555 -> rounds down to 1.555555 (6 decimals) -> 1555555
72+
result = fare.format_amount(1.5555559)
73+
74+
assert result == 1555555
75+
76+
def test_should_handle_18_decimals_for_eth(self):
77+
"""Should format amount with 18 decimals for ETH"""
78+
fare = Fare("0x4200000000000000000000000000000000000006", 18)
79+
80+
result = fare.format_amount(1.0)
81+
82+
assert result == 1000000000000000000 # 1 * 10^18
83+
84+
def test_should_handle_very_small_amounts(self):
85+
"""Should handle very small amounts"""
86+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
87+
88+
result = fare.format_amount(0.000001)
89+
90+
assert result == 1 # 0.000001 * 10^6
91+
92+
def test_should_handle_zero(self):
93+
"""Should handle zero amount"""
94+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
95+
96+
result = fare.format_amount(0)
97+
98+
assert result == 0
99+
100+
class TestFromContractAddress:
101+
"""Test from_contract_address static method"""
102+
103+
def test_should_return_base_fare_when_address_matches(self):
104+
"""Should return base_fare from config when address matches"""
105+
mock_config = MagicMock()
106+
base_fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
107+
mock_config.base_fare = base_fare
108+
109+
result = Fare.from_contract_address(
110+
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
111+
mock_config
112+
)
113+
114+
assert result is base_fare
115+
116+
def test_should_return_base_fare_with_lowercase_address(self):
117+
"""Should match address case-insensitively"""
118+
mock_config = MagicMock()
119+
base_fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
120+
mock_config.base_fare = base_fare
121+
122+
# Use lowercase address
123+
result = Fare.from_contract_address(
124+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
125+
mock_config
126+
)
127+
128+
assert result is base_fare
129+
130+
def test_should_query_blockchain_for_unknown_token(self):
131+
"""Should query blockchain to get decimals for unknown token"""
132+
mock_config = MagicMock()
133+
mock_config.base_fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
134+
mock_config.rpc_url = "https://rpc.example.com"
135+
136+
# Mock Web3 and contract
137+
with patch('virtuals_acp.fare.Web3') as mock_web3_class:
138+
mock_w3 = MagicMock()
139+
mock_web3_class.return_value = mock_w3
140+
mock_web3_class.HTTPProvider = MagicMock()
141+
mock_web3_class.to_checksum_address = lambda x: x.replace("0x", "0x").upper() if "0x" in x else x
142+
143+
mock_contract = MagicMock()
144+
mock_contract.functions.decimals().call.return_value = 18
145+
mock_w3.eth.contract.return_value = mock_contract
146+
147+
result = Fare.from_contract_address(
148+
"0x4200000000000000000000000000000000000006", # Different address
149+
mock_config
150+
)
151+
152+
assert result.decimals == 18
153+
assert mock_contract.functions.decimals().call.called
154+
155+
156+
class TestFareAmountBase:
157+
"""Test suite for FareAmountBase abstract class"""
158+
159+
class TestInitialization:
160+
"""Test FareAmountBase initialization"""
161+
162+
def test_should_initialize_with_amount_and_fare(self):
163+
"""Should initialize with amount and fare"""
164+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
165+
fare_amount = FareBigInt(1000000, fare)
166+
167+
assert fare_amount.amount == 1000000
168+
assert fare_amount.fare is fare
169+
170+
class TestStringRepresentation:
171+
"""Test __repr__ and __str__ methods"""
172+
173+
def test_should_return_formatted_repr(self):
174+
"""Should return formatted representation"""
175+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
176+
fare_amount = FareBigInt(1500000, fare)
177+
178+
result = repr(fare_amount)
179+
180+
assert "FareAmount" in result
181+
assert "1500000" in result
182+
assert "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" in result
183+
assert "decimals=6" in result
184+
185+
def test_should_return_same_string_as_repr(self):
186+
"""Should return same string representation as repr"""
187+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
188+
fare_amount = FareBigInt(1500000, fare)
189+
190+
assert str(fare_amount) == repr(fare_amount)
191+
192+
class TestFromContractAddress:
193+
"""Test from_contract_address static method"""
194+
195+
def test_should_return_fare_amount_for_float(self):
196+
"""Should return FareAmount when amount is float"""
197+
mock_config = MagicMock()
198+
base_fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
199+
mock_config.base_fare = base_fare
200+
201+
result = FareAmountBase.from_contract_address(
202+
1.5,
203+
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
204+
mock_config
205+
)
206+
207+
assert isinstance(result, FareAmount)
208+
assert result.amount == 1500000
209+
210+
def test_should_return_fare_big_int_for_int(self):
211+
"""Should return FareBigInt when amount is int"""
212+
mock_config = MagicMock()
213+
base_fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
214+
mock_config.base_fare = base_fare
215+
216+
result = FareAmountBase.from_contract_address(
217+
1000000,
218+
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
219+
mock_config
220+
)
221+
222+
assert isinstance(result, FareBigInt)
223+
assert result.amount == 1000000
224+
225+
226+
class TestFareAmount:
227+
"""Test suite for FareAmount class"""
228+
229+
class TestInitialization:
230+
"""Test FareAmount initialization"""
231+
232+
def test_should_initialize_with_float_amount(self):
233+
"""Should initialize with float amount"""
234+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
235+
236+
fare_amount = FareAmount(1.5, fare)
237+
238+
assert fare_amount.amount == 1500000 # 1.5 * 10^6
239+
assert fare_amount.fare is fare
240+
241+
def test_should_initialize_with_integer_amount(self):
242+
"""Should initialize with integer amount"""
243+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
244+
245+
fare_amount = FareAmount(100, fare)
246+
247+
assert fare_amount.amount == 100000000 # 100 * 10^6
248+
249+
def test_should_truncate_to_6_decimals(self):
250+
"""Should truncate amount to 6 decimal places"""
251+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
252+
253+
# 1.5555559 should be truncated to 1.555555
254+
fare_amount = FareAmount(1.5555559, fare)
255+
256+
assert fare_amount.amount == 1555555 # 1.555555 * 10^6
257+
258+
def test_should_handle_very_small_amounts(self):
259+
"""Should handle very small amounts"""
260+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
261+
262+
fare_amount = FareAmount(0.000001, fare)
263+
264+
assert fare_amount.amount == 1
265+
266+
def test_should_handle_large_amounts_with_18_decimals(self):
267+
"""Should handle large amounts with 18 decimals"""
268+
fare = Fare("0x4200000000000000000000000000000000000006", 18)
269+
270+
fare_amount = FareAmount(1.0, fare)
271+
272+
assert fare_amount.amount == 1000000000000000000
273+
274+
class TestAdd:
275+
"""Test add method"""
276+
277+
def test_should_add_two_fare_amounts(self):
278+
"""Should add two fare amounts with same token"""
279+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
280+
fare_amount1 = FareAmount(1.5, fare)
281+
fare_amount2 = FareAmount(2.5, fare)
282+
283+
result = fare_amount1.add(fare_amount2)
284+
285+
assert isinstance(result, FareBigInt)
286+
assert result.amount == 4000000 # (1.5 + 2.5) * 10^6
287+
288+
def test_should_add_fare_amount_and_fare_big_int(self):
289+
"""Should add FareAmount and FareBigInt"""
290+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
291+
fare_amount = FareAmount(1.5, fare)
292+
fare_big_int = FareBigInt(1000000, fare)
293+
294+
result = fare_amount.add(fare_big_int)
295+
296+
assert isinstance(result, FareBigInt)
297+
assert result.amount == 2500000 # 1.5 * 10^6 + 1000000
298+
299+
def test_should_raise_error_when_tokens_do_not_match(self):
300+
"""Should raise ACPError when token addresses do not match"""
301+
fare1 = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
302+
fare2 = Fare("0x4200000000000000000000000000000000000006", 18)
303+
fare_amount1 = FareAmount(1.5, fare1)
304+
fare_amount2 = FareAmount(2.5, fare2)
305+
306+
with pytest.raises(ACPError, match="Token addresses do not match"):
307+
fare_amount1.add(fare_amount2)
308+
309+
310+
class TestFareBigInt:
311+
"""Test suite for FareBigInt class"""
312+
313+
class TestInitialization:
314+
"""Test FareBigInt initialization"""
315+
316+
def test_should_initialize_with_integer_amount(self):
317+
"""Should initialize with integer amount"""
318+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
319+
320+
fare_big_int = FareBigInt(1000000, fare)
321+
322+
assert fare_big_int.amount == 1000000
323+
assert fare_big_int.fare is fare
324+
325+
def test_should_store_amount_as_is(self):
326+
"""Should store amount without formatting"""
327+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
328+
329+
fare_big_int = FareBigInt(123456789, fare)
330+
331+
assert fare_big_int.amount == 123456789
332+
333+
class TestAdd:
334+
"""Test add method"""
335+
336+
def test_should_add_two_fare_big_ints(self):
337+
"""Should add two FareBigInt amounts"""
338+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
339+
fare_big_int1 = FareBigInt(1000000, fare)
340+
fare_big_int2 = FareBigInt(2000000, fare)
341+
342+
result = fare_big_int1.add(fare_big_int2)
343+
344+
assert isinstance(result, FareBigInt)
345+
assert result.amount == 3000000
346+
347+
def test_should_add_fare_big_int_and_fare_amount(self):
348+
"""Should add FareBigInt and FareAmount"""
349+
fare = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
350+
fare_big_int = FareBigInt(1000000, fare)
351+
fare_amount = FareAmount(1.5, fare)
352+
353+
result = fare_big_int.add(fare_amount)
354+
355+
assert isinstance(result, FareBigInt)
356+
assert result.amount == 2500000 # 1000000 + 1.5 * 10^6
357+
358+
def test_should_raise_error_when_tokens_do_not_match(self):
359+
"""Should raise ACPError when token addresses do not match"""
360+
fare1 = Fare("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 6)
361+
fare2 = Fare("0x4200000000000000000000000000000000000006", 18)
362+
fare_big_int1 = FareBigInt(1000000, fare1)
363+
fare_big_int2 = FareBigInt(2000000, fare2)
364+
365+
with pytest.raises(ACPError, match="Token addresses do not match"):
366+
fare_big_int1.add(fare_big_int2)
367+
368+
369+
class TestPredeclaredFares:
370+
"""Test predeclared fare instances"""
371+
372+
def test_weth_fare_should_be_defined(self):
373+
"""Should have WETH_FARE predeclared"""
374+
assert WETH_FARE.contract_address == "0x4200000000000000000000000000000000000006"
375+
assert WETH_FARE.decimals == 18
376+
377+
def test_eth_fare_should_be_defined(self):
378+
"""Should have ETH_FARE predeclared"""
379+
assert ETH_FARE.contract_address == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
380+
assert ETH_FARE.decimals == 18

0 commit comments

Comments
 (0)