feat: add foreign key constraints and metrics for blockchain node
This commit is contained in:
23
apps/blockchain-node/tests/conftest.py
Normal file
23
apps/blockchain-node/tests/conftest.py
Normal file
@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from sqlmodel import SQLModel, Session, create_engine
|
||||
|
||||
from aitbc_chain.models import Block, Transaction, Receipt # noqa: F401 - ensure models imported for metadata
|
||||
|
||||
|
||||
@pytest.fixture(name="engine")
|
||||
def engine_fixture():
|
||||
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||
SQLModel.metadata.create_all(engine)
|
||||
try:
|
||||
yield engine
|
||||
finally:
|
||||
SQLModel.metadata.drop_all(engine)
|
||||
|
||||
|
||||
@pytest.fixture(name="session")
|
||||
def session_fixture(engine):
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
session.rollback()
|
||||
76
apps/blockchain-node/tests/test_gossip_broadcast.py
Normal file
76
apps/blockchain-node/tests/test_gossip_broadcast.py
Normal file
@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from aitbc_chain.app import create_app
|
||||
from aitbc_chain.gossip import BroadcastGossipBackend, InMemoryGossipBackend, gossip_broker
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def reset_broker_backend():
|
||||
previous_backend = InMemoryGossipBackend()
|
||||
await gossip_broker.set_backend(previous_backend)
|
||||
yield
|
||||
await gossip_broker.set_backend(InMemoryGossipBackend())
|
||||
|
||||
|
||||
def _run_in_thread(fn):
|
||||
loop = asyncio.get_event_loop()
|
||||
return loop.run_in_executor(None, fn)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_fanout_with_broadcast_backend():
|
||||
backend = BroadcastGossipBackend("memory://")
|
||||
await gossip_broker.set_backend(backend)
|
||||
|
||||
app = create_app()
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
def _sync_test() -> None:
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/rpc/ws/transactions") as ws_a, client.websocket_connect(
|
||||
"/rpc/ws/transactions"
|
||||
) as ws_b:
|
||||
payload = {
|
||||
"tx_hash": "0x01",
|
||||
"sender": "alice",
|
||||
"recipient": "bob",
|
||||
"payload": {"amount": 1},
|
||||
"nonce": 0,
|
||||
"fee": 0,
|
||||
"type": "TRANSFER",
|
||||
}
|
||||
fut = asyncio.run_coroutine_threadsafe(gossip_broker.publish("transactions", payload), loop)
|
||||
fut.result(timeout=5.0)
|
||||
assert ws_a.receive_json() == payload
|
||||
assert ws_b.receive_json() == payload
|
||||
|
||||
await _run_in_thread(_sync_test)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_broadcast_backend_decodes_cursorless_payload():
|
||||
backend = BroadcastGossipBackend("memory://")
|
||||
await gossip_broker.set_backend(backend)
|
||||
|
||||
app = create_app()
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
def _sync_test() -> None:
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/rpc/ws/blocks") as ws:
|
||||
payload = [
|
||||
{"height": 1, "hash": "0xabc"},
|
||||
{"height": 2, "hash": "0xdef"},
|
||||
]
|
||||
fut = asyncio.run_coroutine_threadsafe(gossip_broker.publish("blocks", payload), loop)
|
||||
fut.result(timeout=5.0)
|
||||
assert ws.receive_json() == payload
|
||||
|
||||
await _run_in_thread(_sync_test)
|
||||
92
apps/blockchain-node/tests/test_models.py
Normal file
92
apps/blockchain-node/tests/test_models.py
Normal file
@ -0,0 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from sqlmodel import Session
|
||||
|
||||
from aitbc_chain.models import Block, Transaction, Receipt
|
||||
|
||||
|
||||
def _insert_block(session: Session, height: int = 0) -> Block:
|
||||
block = Block(
|
||||
height=height,
|
||||
hash=f"0x{'0'*63}{height}",
|
||||
parent_hash="0x" + "0" * 64,
|
||||
proposer="validator",
|
||||
tx_count=0,
|
||||
)
|
||||
session.add(block)
|
||||
session.commit()
|
||||
session.refresh(block)
|
||||
return block
|
||||
|
||||
|
||||
def test_relationships(session: Session) -> None:
|
||||
block = _insert_block(session, height=1)
|
||||
|
||||
tx = Transaction(
|
||||
tx_hash="0x" + "1" * 64,
|
||||
block_height=block.height,
|
||||
sender="alice",
|
||||
recipient="bob",
|
||||
payload={"foo": "bar"},
|
||||
)
|
||||
receipt = Receipt(
|
||||
job_id="job-1",
|
||||
receipt_id="0x" + "2" * 64,
|
||||
block_height=block.height,
|
||||
payload={},
|
||||
miner_signature={},
|
||||
coordinator_attestations=[],
|
||||
)
|
||||
session.add(tx)
|
||||
session.add(receipt)
|
||||
session.commit()
|
||||
session.refresh(tx)
|
||||
session.refresh(receipt)
|
||||
|
||||
assert tx.block is not None
|
||||
assert tx.block.hash == block.hash
|
||||
assert receipt.block is not None
|
||||
assert receipt.block.hash == block.hash
|
||||
|
||||
|
||||
def test_hash_validation_accepts_hex(session: Session) -> None:
|
||||
block = Block(
|
||||
height=10,
|
||||
hash="0x" + "a" * 64,
|
||||
parent_hash="0x" + "b" * 64,
|
||||
proposer="validator",
|
||||
)
|
||||
session.add(block)
|
||||
session.commit()
|
||||
session.refresh(block)
|
||||
|
||||
assert block.hash.startswith("0x")
|
||||
assert block.parent_hash.startswith("0x")
|
||||
|
||||
|
||||
def test_hash_validation_rejects_non_hex(session: Session) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
Block(
|
||||
height=20,
|
||||
hash="not-hex",
|
||||
parent_hash="0x" + "c" * 64,
|
||||
proposer="validator",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
Transaction(
|
||||
tx_hash="bad",
|
||||
sender="alice",
|
||||
recipient="bob",
|
||||
payload={},
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
Receipt(
|
||||
job_id="job",
|
||||
receipt_id="oops",
|
||||
payload={},
|
||||
miner_signature={},
|
||||
coordinator_attestations=[],
|
||||
)
|
||||
46
apps/blockchain-node/tests/test_websocket.py
Normal file
46
apps/blockchain-node/tests/test_websocket.py
Normal file
@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from aitbc_chain.app import create_app
|
||||
from aitbc_chain.gossip import gossip_broker
|
||||
|
||||
|
||||
def _publish(topic: str, message: dict) -> None:
|
||||
asyncio.run(gossip_broker.publish(topic, message))
|
||||
|
||||
|
||||
def test_blocks_websocket_stream() -> None:
|
||||
client = TestClient(create_app())
|
||||
|
||||
with client.websocket_connect("/rpc/ws/blocks") as websocket:
|
||||
payload = {
|
||||
"height": 1,
|
||||
"hash": "0x" + "1" * 64,
|
||||
"parent_hash": "0x" + "0" * 64,
|
||||
"timestamp": "2025-01-01T00:00:00Z",
|
||||
"tx_count": 2,
|
||||
}
|
||||
_publish("blocks", payload)
|
||||
message = websocket.receive_json()
|
||||
assert message == payload
|
||||
|
||||
|
||||
def test_transactions_websocket_stream() -> None:
|
||||
client = TestClient(create_app())
|
||||
|
||||
with client.websocket_connect("/rpc/ws/transactions") as websocket:
|
||||
payload = {
|
||||
"tx_hash": "0x" + "a" * 64,
|
||||
"sender": "alice",
|
||||
"recipient": "bob",
|
||||
"payload": {"amount": 1},
|
||||
"nonce": 1,
|
||||
"fee": 0,
|
||||
"type": "TRANSFER",
|
||||
}
|
||||
_publish("transactions", payload)
|
||||
message = websocket.receive_json()
|
||||
assert message == payload
|
||||
Reference in New Issue
Block a user