Skip to content

Commit 7c9961c

Browse files
committed
test: restore classic test suite at project root (21 files)
1 parent 64527f9 commit 7c9961c

21 files changed

+5510
-0
lines changed

tests/spend_vector.py

Lines changed: 2293 additions & 0 deletions
Large diffs are not rendered by default.

tests/test_aes_cbc.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from secrets import randbits
2+
3+
import pytest
4+
5+
from bsv.aes_cbc import InvalidPadding
6+
from bsv.aes_cbc import append_pkcs7_padding, strip_pkcs7_padding, aes_encrypt_with_iv, aes_decrypt_with_iv
7+
8+
9+
def test():
10+
message: bytes = b'hello world'
11+
padding_message: bytes = b'hello world\x05\x05\x05\x05\x05'
12+
assert append_pkcs7_padding(message) == padding_message
13+
assert strip_pkcs7_padding(padding_message) == message
14+
15+
message: bytes = b'\x00' * 16
16+
padding_message: bytes = message + b'\x10' * 16
17+
assert append_pkcs7_padding(message) == padding_message
18+
assert strip_pkcs7_padding(padding_message) == message
19+
20+
with pytest.raises(InvalidPadding, match=r'invalid length'):
21+
strip_pkcs7_padding(b'')
22+
with pytest.raises(InvalidPadding, match=r'invalid length'):
23+
strip_pkcs7_padding(b'\x00' * 15)
24+
with pytest.raises(InvalidPadding, match=r'invalid padding byte \(out of range\)'):
25+
strip_pkcs7_padding(b'hello world\x05\x05\x05\x05\xff')
26+
with pytest.raises(InvalidPadding, match=r'invalid padding byte \(inconsistent\)'):
27+
strip_pkcs7_padding(b'hello world\x05\x05\x05\x04\x05')
28+
29+
key_byte_length = 16
30+
key = randbits(key_byte_length * 8).to_bytes(key_byte_length, 'big')
31+
iv = randbits(key_byte_length * 8).to_bytes(key_byte_length, 'big')
32+
encrypted: bytes = aes_encrypt_with_iv(key, iv, message)
33+
assert message == aes_decrypt_with_iv(key, iv, encrypted)

tests/test_arc.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import unittest
2+
from unittest.mock import AsyncMock, MagicMock
3+
4+
from bsv.broadcaster import BroadcastResponse, BroadcastFailure
5+
from bsv.broadcasters.arc import ARC, ARCConfig
6+
from bsv.http_client import HttpClient, HttpResponse, SyncHttpClient
7+
from bsv.transaction import Transaction
8+
9+
10+
class TestARCBroadcast(unittest.IsolatedAsyncioTestCase):
11+
12+
def setUp(self):
13+
self.URL = "https://api.taal.com/arc"
14+
self.api_key = "apikey_85678993923y454i4jhd803wsd02"
15+
self.tx = Transaction(tx_data="Hello sCrypt")
16+
17+
# Mocking the Transaction methods
18+
self.tx.hex = MagicMock(return_value="hexFormat")
19+
20+
async def test_broadcast_success(self):
21+
mock_response = HttpResponse(
22+
ok=True,
23+
status_code=200,
24+
json_data={
25+
"data": {
26+
"txid": "8e60c4143879918ed03b8fc67b5ac33b8187daa3b46022ee2a9e1eb67e2e46ec",
27+
"txStatus": "success",
28+
"extraInfo": "extra",
29+
}
30+
},
31+
)
32+
mock_http_client = AsyncMock(HttpClient)
33+
mock_http_client.fetch = AsyncMock(return_value=mock_response)
34+
35+
arc_config = ARCConfig(api_key=self.api_key, http_client=mock_http_client)
36+
arc = ARC(self.URL, arc_config)
37+
result = await arc.broadcast(self.tx)
38+
39+
self.assertIsInstance(result, BroadcastResponse)
40+
self.assertEqual(
41+
result.txid,
42+
"8e60c4143879918ed03b8fc67b5ac33b8187daa3b46022ee2a9e1eb67e2e46ec",
43+
)
44+
self.assertEqual(result.message, "success extra")
45+
46+
async def test_broadcast_failure(self):
47+
mock_response = HttpResponse(
48+
ok=False,
49+
status_code=400,
50+
json_data={
51+
"data": {"status": "ERR_BAD_REQUEST", "detail": "Invalid transaction"}
52+
},
53+
)
54+
mock_http_client = AsyncMock(HttpClient)
55+
mock_http_client.fetch = AsyncMock(return_value=mock_response)
56+
57+
arc_config = ARCConfig(api_key=self.api_key, http_client=mock_http_client)
58+
arc = ARC(self.URL, arc_config)
59+
result = await arc.broadcast(self.tx)
60+
61+
self.assertIsInstance(result, BroadcastFailure)
62+
self.assertEqual(result.code, "400")
63+
self.assertEqual(result.description, "Invalid transaction")
64+
65+
async def test_broadcast_exception(self):
66+
mock_http_client = AsyncMock(HttpClient)
67+
mock_http_client.fetch = AsyncMock(side_effect=Exception("Internal Error"))
68+
69+
arc_config = ARCConfig(api_key=self.api_key, http_client=mock_http_client)
70+
arc = ARC(self.URL, arc_config)
71+
result = await arc.broadcast(self.tx)
72+
73+
self.assertIsInstance(result, BroadcastFailure)
74+
self.assertEqual(result.code, "500")
75+
self.assertEqual(result.description, "Internal Error")
76+
77+
def test_sync_broadcast_success(self):
78+
mock_response = HttpResponse(
79+
ok=True,
80+
status_code=200,
81+
json_data={
82+
"data": {
83+
"txid": "8e60c4143879918ed03b8fc67b5ac33b8187daa3b46022ee2a9e1eb67e2e46ec",
84+
"txStatus": "success",
85+
"extraInfo": "extra",
86+
}
87+
},
88+
)
89+
mock_sync_http_client = MagicMock(SyncHttpClient)
90+
mock_sync_http_client.post = MagicMock(return_value=mock_response) # fetch → post
91+
92+
arc_config = ARCConfig(api_key=self.api_key, sync_http_client=mock_sync_http_client)
93+
arc = ARC(self.URL, arc_config)
94+
result = arc.sync_broadcast(self.tx)
95+
96+
self.assertIsInstance(result, BroadcastResponse)
97+
self.assertEqual(
98+
result.txid,
99+
"8e60c4143879918ed03b8fc67b5ac33b8187daa3b46022ee2a9e1eb67e2e46ec",
100+
)
101+
self.assertEqual(result.message, "success extra")
102+
103+
def test_sync_broadcast_failure(self):
104+
mock_response = HttpResponse(
105+
ok=False,
106+
status_code=400,
107+
json_data={
108+
"data": {"status": "ERR_BAD_REQUEST", "detail": "Invalid transaction"}
109+
},
110+
)
111+
mock_sync_http_client = MagicMock(SyncHttpClient)
112+
mock_sync_http_client.post = MagicMock(return_value=mock_response) # fetch → post
113+
114+
arc_config = ARCConfig(api_key=self.api_key, sync_http_client=mock_sync_http_client)
115+
arc = ARC(self.URL, arc_config)
116+
result = arc.sync_broadcast(self.tx)
117+
118+
self.assertIsInstance(result, BroadcastFailure)
119+
self.assertEqual(result.code, "400")
120+
self.assertEqual(result.description, "Invalid transaction")
121+
122+
def test_sync_broadcast_timeout_error(self):
123+
"""408 time out error test"""
124+
mock_response = HttpResponse(
125+
ok=False,
126+
status_code=408,
127+
json_data={"data": {"status": "ERR_TIMEOUT", "detail": "Request timed out"}}
128+
)
129+
mock_sync_http_client = MagicMock(SyncHttpClient)
130+
mock_sync_http_client.post = MagicMock(return_value=mock_response)
131+
132+
arc_config = ARCConfig(api_key=self.api_key, sync_http_client=mock_sync_http_client)
133+
arc = ARC(self.URL, arc_config)
134+
result = arc.sync_broadcast(self.tx, timeout=5)
135+
136+
self.assertIsInstance(result, BroadcastFailure)
137+
self.assertEqual(result.status, "failure")
138+
self.assertEqual(result.code, "408")
139+
self.assertEqual(result.description, "Transaction broadcast timed out after 5 seconds")
140+
141+
def test_sync_broadcast_connection_error(self):
142+
"""503 error test"""
143+
mock_response = HttpResponse(
144+
ok=False,
145+
status_code=503,
146+
json_data={"data": {"status": "ERR_CONNECTION", "detail": "Service unavailable"}}
147+
)
148+
mock_sync_http_client = MagicMock(SyncHttpClient)
149+
mock_sync_http_client.post = MagicMock(return_value=mock_response)
150+
151+
arc_config = ARCConfig(api_key=self.api_key, sync_http_client=mock_sync_http_client)
152+
arc = ARC(self.URL, arc_config)
153+
result = arc.sync_broadcast(self.tx)
154+
155+
self.assertIsInstance(result, BroadcastFailure)
156+
self.assertEqual(result.status, "failure")
157+
self.assertEqual(result.code, "503")
158+
self.assertEqual(result.description, "Failed to connect to ARC service")
159+
160+
def test_sync_broadcast_exception(self):
161+
mock_sync_http_client = MagicMock(SyncHttpClient)
162+
mock_sync_http_client.post = MagicMock(side_effect=Exception("Internal Error"))
163+
164+
arc_config = ARCConfig(api_key=self.api_key, sync_http_client=mock_sync_http_client)
165+
arc = ARC(self.URL, arc_config)
166+
result = arc.sync_broadcast(self.tx)
167+
168+
self.assertIsInstance(result, BroadcastFailure)
169+
self.assertEqual(result.code, "500")
170+
self.assertEqual(result.description, "Internal Error")
171+
172+
def test_check_transaction_status_success(self):
173+
mock_response = HttpResponse(
174+
ok=True,
175+
status_code=200,
176+
json_data={
177+
"data": { # dataキーを追加
178+
"txid": "8e60c4143879918ed03b8fc67b5ac33b8187daa3b46022ee2a9e1eb67e2e46ec",
179+
"txStatus": "MINED",
180+
"blockHash": "000000000000000001234567890abcdef",
181+
"blockHeight": 800000
182+
}
183+
},
184+
)
185+
mock_sync_http_client = MagicMock(SyncHttpClient)
186+
mock_sync_http_client.get = MagicMock(return_value=mock_response) # fetch → get
187+
188+
arc_config = ARCConfig(api_key=self.api_key, sync_http_client=mock_sync_http_client)
189+
arc = ARC(self.URL, arc_config)
190+
result = arc.check_transaction_status("8e60c4143879918ed03b8fc67b5ac33b8187daa3b46022ee2a9e1eb67e2e46ec")
191+
192+
self.assertEqual(result["txid"], "8e60c4143879918ed03b8fc67b5ac33b8187daa3b46022ee2a9e1eb67e2e46ec")
193+
self.assertEqual(result["txStatus"], "MINED")
194+
self.assertEqual(result["blockHeight"], 800000)
195+
196+
def test_categorize_transaction_status_mined(self):
197+
response = {
198+
"txStatus": "MINED",
199+
"blockHeight": 800000
200+
}
201+
result = ARC.categorize_transaction_status(response)
202+
203+
self.assertEqual(result["status_category"], "mined")
204+
self.assertEqual(result["tx_status"], "MINED")
205+
206+
def test_categorize_transaction_status_progressing(self):
207+
response = {
208+
"txStatus": "QUEUED"
209+
}
210+
result = ARC.categorize_transaction_status(response)
211+
212+
self.assertEqual(result["status_category"], "progressing")
213+
self.assertEqual(result["tx_status"], "QUEUED")
214+
215+
def test_categorize_transaction_status_warning(self):
216+
response = {
217+
"txStatus": "SEEN_ON_NETWORK",
218+
"competingTxs": ["some_competing_tx"]
219+
}
220+
result = ARC.categorize_transaction_status(response)
221+
222+
self.assertEqual(result["status_category"], "warning")
223+
self.assertEqual(result["tx_status"], "SEEN_ON_NETWORK")
224+
225+
def test_categorize_transaction_status_0confirmation(self):
226+
response = {
227+
"txStatus": "SEEN_ON_NETWORK"
228+
}
229+
result = ARC.categorize_transaction_status(response)
230+
231+
self.assertEqual(result["status_category"], "0confirmation")
232+
self.assertEqual(result["tx_status"], "SEEN_ON_NETWORK")
233+
234+
235+
if __name__ == "__main__":
236+
unittest.main()

tests/test_arc_ef_or_rawhex.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import unittest
2+
from unittest.mock import MagicMock, patch
3+
from typing import Union, List
4+
5+
6+
# テスト対象のクラスとメソッドをモックで再現
7+
class Transaction:
8+
def __init__(self, inputs=None):
9+
self.inputs = inputs or []
10+
11+
def to_ef(self):
12+
# EFフォーマットに変換するメソッドをモック
13+
mock = MagicMock()
14+
mock.hex.return_value = "ef_formatted_hex_data"
15+
return mock
16+
17+
def hex(self):
18+
return "normal_hex_data"
19+
20+
21+
class Input:
22+
def __init__(self, source_transaction=None):
23+
self.source_transaction = source_transaction
24+
25+
26+
class BroadcastResponse:
27+
pass
28+
29+
30+
class BroadcastFailure:
31+
pass
32+
33+
34+
class TransactionBroadcaster:
35+
def request_headers(self):
36+
return {"Content-Type": "application/json"}
37+
38+
async def broadcast(self, tx: 'Transaction') -> Union[BroadcastResponse, BroadcastFailure]:
39+
# Check if all inputs have source_transaction
40+
has_all_source_txs = all(input.source_transaction is not None for input in tx.inputs)
41+
request_options = {
42+
"method": "POST",
43+
"headers": self.request_headers(),
44+
"data": {
45+
"rawTx": tx.to_ef().hex() if has_all_source_txs else tx.hex()
46+
}
47+
}
48+
return request_options # テスト用に結果を返す
49+
50+
51+
# ユニットテスト
52+
class TestTransactionBroadcaster(unittest.TestCase):
53+
def setUp(self):
54+
self.broadcaster = TransactionBroadcaster()
55+
56+
async def test_all_inputs_have_source_transaction(self):
57+
# すべての入力にsource_transactionがある場合
58+
inputs = [
59+
Input(source_transaction="tx1"),
60+
Input(source_transaction="tx2"),
61+
Input(source_transaction="tx3")
62+
]
63+
tx = Transaction(inputs=inputs)
64+
65+
result = await self.broadcaster.broadcast(tx)
66+
67+
# EFフォーマットが使われていることを確認
68+
self.assertEqual(result["data"]["rawTx"], "ef_formatted_hex_data")
69+
70+
async def test_some_inputs_missing_source_transaction(self):
71+
# 一部の入力にsource_transactionがない場合
72+
inputs = [
73+
Input(source_transaction="tx1"),
74+
Input(source_transaction=None), # source_transactionがない
75+
Input(source_transaction="tx3")
76+
]
77+
tx = Transaction(inputs=inputs)
78+
79+
result = await self.broadcaster.broadcast(tx)
80+
81+
# 通常のhexフォーマットが使われていることを確認
82+
self.assertEqual(result["data"]["rawTx"], "normal_hex_data")
83+
84+
async def test_no_inputs_have_source_transaction(self):
85+
# すべての入力にsource_transactionがない場合
86+
inputs = [
87+
Input(source_transaction=None),
88+
Input(source_transaction=None),
89+
Input(source_transaction=None)
90+
]
91+
tx = Transaction(inputs=inputs)
92+
93+
result = await self.broadcaster.broadcast(tx)
94+
95+
# 通常のhexフォーマットが使われていることを確認
96+
self.assertEqual(result["data"]["rawTx"], "normal_hex_data")
97+
98+
99+
# 非同期テストを実行するためのヘルパー関数
100+
import asyncio
101+
102+
103+
def run_async_test(test_case):
104+
async_test = getattr(test_case, test_case._testMethodName)
105+
asyncio.run(async_test())
106+
107+
108+
if __name__ == '__main__':
109+
unittest.main()

0 commit comments

Comments
 (0)