Skip to content

Commit 973b0bb

Browse files
author
Matthias Zimmermann
committed
add separate tests for container and external arkiv nodes
1 parent ff3c544 commit 973b0bb

File tree

3 files changed

+270
-80
lines changed

3 files changed

+270
-80
lines changed

src/arkiv/node.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ def __init__(
8787
image: str | None = None,
8888
http_port: int | None = None,
8989
ws_port: int | None = None,
90-
auto_start: bool = False,
9190
http_url: str | None = None,
9291
ws_url: str | None = None,
9392
) -> None:
@@ -98,7 +97,6 @@ def __init__(
9897
image: Docker image to use (default: golemnetwork/golembase-op-geth:latest)
9998
http_port: Internal HTTP port (default: 8545)
10099
ws_port: Internal WebSocket port (default: 8546)
101-
auto_start: Automatically start the node on initialization (default: False)
102100
http_url: External HTTP RPC URL (for external nodes, disables container)
103101
ws_url: External WebSocket RPC URL (for external nodes, disables container)
104102
@@ -142,9 +140,6 @@ def __init__(
142140
self._http_port = http_port or self.DEFAULT_HTTP_PORT
143141
self._ws_port = ws_port or self.DEFAULT_WS_PORT
144142

145-
if auto_start:
146-
self.start()
147-
148143
@property
149144
def http_url(self) -> str:
150145
"""

tests/conftest.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,37 @@ def _load_env_if_available() -> None:
5050
_load_env_if_available()
5151

5252

53+
@pytest.fixture(scope="session")
54+
def arkiv_container() -> Generator[ArkivNode, None, None]:
55+
"""
56+
Provide a containerized ArkivNode for testing.
57+
58+
Creates and manages a local Docker-based Arkiv node.
59+
Use this fixture for tests that need container-specific features.
60+
"""
61+
logger.info("Creating containerized test ArkivNode...")
62+
with ArkivNode() as node:
63+
logger.info(f"Test node ready at {node.http_url}")
64+
yield node
65+
66+
67+
@pytest.fixture(scope="session")
68+
def arkiv_testnet() -> ArkivNode:
69+
"""
70+
Provide Kaolin testnet ArkivNode for testing.
71+
72+
Always points to the Kaolin testnet.
73+
Use this fixture for tests that work with the public Kaolin testnet.
74+
"""
75+
from arkiv.provider import HTTP, KAOLIN, NETWORK_URL, WS
76+
77+
http_url = NETWORK_URL[KAOLIN][HTTP]
78+
ws_url = NETWORK_URL[KAOLIN][WS]
79+
80+
logger.info(f"Using Kaolin testnet: {http_url}")
81+
return ArkivNode(http_url=http_url, ws_url=ws_url)
82+
83+
5384
@pytest.fixture(scope="session")
5485
def arkiv_node() -> Generator[ArkivNode, None, None]:
5586
"""
@@ -58,6 +89,7 @@ def arkiv_node() -> Generator[ArkivNode, None, None]:
5889
If RPC_URL and WS_URL are set in environment, creates an ArkivNode
5990
configured for the external node (no container management).
6091
Otherwise, creates and manages a local containerized node.
92+
Use this fixture for tests that work with either node type.
6193
"""
6294
# Check for external node configuration
6395
rpc_url = os.getenv(RPC_URL_ENV)

tests/test_node.py

Lines changed: 238 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -12,78 +12,241 @@
1212
logger = logging.getLogger(__name__)
1313

1414

15-
def test_node_connection_http(arkiv_node: ArkivNode) -> None:
16-
"""Check if the Arkiv node is available and responsive via JSON-RPC."""
17-
# Use JSON-RPC call - works for both dev and production nodes
18-
rpc_payload = {"jsonrpc": "2.0", "method": "eth_chainId", "params": [], "id": 1}
19-
20-
response = requests.post(
21-
arkiv_node.http_url,
22-
json=rpc_payload,
23-
headers={"Content-Type": "application/json"},
24-
timeout=10,
25-
)
26-
27-
assert response.status_code == 200, (
28-
f"Arkiv node should respond with 200 OK, got {response.status_code}"
29-
)
30-
31-
# Verify it's a proper JSON-RPC response
32-
json_response = response.json()
33-
assert "result" in json_response or "error" in json_response, (
34-
"Response should contain either 'result' or 'error' field"
35-
)
36-
assert json_response.get("jsonrpc") == "2.0", (
37-
"Response should have jsonrpc version 2.0"
38-
)
39-
40-
logger.info(f"HTTP connection successful: {arkiv_node.http_url}")
41-
logger.info(f"Request response: {json_response}")
42-
43-
44-
def test_node_connection_ws(arkiv_node: ArkivNode) -> None:
45-
"""Check if the Arkiv node WebSocket endpoint is available and responsive."""
46-
# Try to import websockets, skip test if not available
47-
try:
48-
import websockets
49-
except ImportError:
50-
pytest.skip("websockets package not available")
51-
52-
async def test_ws_connection() -> dict[str, object]:
53-
"""Test WebSocket connection and send a JSON-RPC request."""
54-
async with websockets.connect(arkiv_node.ws_url, open_timeout=5) as websocket:
55-
# Send a JSON-RPC request over WebSocket
56-
request = {"jsonrpc": "2.0", "method": "eth_chainId", "params": [], "id": 1}
57-
58-
await websocket.send(json.dumps(request))
59-
response = await websocket.recv()
60-
61-
return json.loads(response) # type: ignore[no-any-return]
62-
63-
# Run the async test
64-
response = asyncio.run(test_ws_connection())
65-
logger.info(f"WebSocket response: {response}")
66-
67-
# Verify the WebSocket JSON-RPC response
68-
assert "result" in response or "error" in response, (
69-
"WebSocket response should contain either 'result' or 'error' field"
70-
)
71-
assert response.get("jsonrpc") == "2.0", (
72-
"WebSocket response should have jsonrpc version 2.0"
73-
)
74-
assert response.get("id") == 1, "WebSocket response should have matching request id"
75-
76-
logger.info(f"WebSocket connection successful: {arkiv_node.ws_url}")
77-
logger.info(f"Chain ID response: {response.get('result', 'N/A')}")
78-
79-
80-
def test_node_arkiv_help(arkiv_node: ArkivNode) -> None:
81-
"""Check if the Arkiv node help command is available and responsive."""
82-
if arkiv_node.is_external():
83-
pytest.skip("Skipping help command test for external nodes")
84-
85-
help_command = ["golembase", "account", "help"]
86-
exit_code, output = arkiv_node.container.exec(help_command)
87-
logger.info(
88-
f"Account help command: {help_command}, exit_code: {exit_code}, output:\n{output.decode()}"
89-
)
15+
class TestArkivNodeConnections:
16+
"""Test basic node connectivity - works for both containerized and external nodes."""
17+
18+
def test_node_connection_http(self, arkiv_node: ArkivNode) -> None:
19+
"""Check if the Arkiv node is available and responsive via JSON-RPC."""
20+
# Use JSON-RPC call - works for both dev and production nodes
21+
rpc_payload = {"jsonrpc": "2.0", "method": "eth_chainId", "params": [], "id": 1}
22+
23+
response = requests.post(
24+
arkiv_node.http_url,
25+
json=rpc_payload,
26+
headers={"Content-Type": "application/json"},
27+
timeout=10,
28+
)
29+
30+
assert response.status_code == 200, (
31+
f"Arkiv node should respond with 200 OK, got {response.status_code}"
32+
)
33+
34+
# Verify it's a proper JSON-RPC response
35+
json_response = response.json()
36+
assert "result" in json_response or "error" in json_response, (
37+
"Response should contain either 'result' or 'error' field"
38+
)
39+
assert json_response.get("jsonrpc") == "2.0", (
40+
"Response should have jsonrpc version 2.0"
41+
)
42+
43+
logger.info(f"HTTP connection successful: {arkiv_node.http_url}")
44+
logger.info(f"Request response: {json_response}")
45+
46+
def test_node_connection_ws(self, arkiv_node: ArkivNode) -> None:
47+
"""Check if the Arkiv node WebSocket endpoint is available and responsive."""
48+
# Try to import websockets, skip test if not available
49+
try:
50+
import websockets
51+
except ImportError:
52+
pytest.skip("websockets package not available")
53+
54+
async def test_ws_connection() -> dict[str, object]:
55+
"""Test WebSocket connection and send a JSON-RPC request."""
56+
async with websockets.connect(
57+
arkiv_node.ws_url, open_timeout=5
58+
) as websocket:
59+
# Send a JSON-RPC request over WebSocket
60+
request = {
61+
"jsonrpc": "2.0",
62+
"method": "eth_chainId",
63+
"params": [],
64+
"id": 1,
65+
}
66+
67+
await websocket.send(json.dumps(request))
68+
response = await websocket.recv()
69+
70+
return json.loads(response) # type: ignore[no-any-return]
71+
72+
# Run the async test
73+
response = asyncio.run(test_ws_connection())
74+
logger.info(f"WebSocket response: {response}")
75+
76+
# Verify the WebSocket JSON-RPC response
77+
assert "result" in response or "error" in response, (
78+
"WebSocket response should contain either 'result' or 'error' field"
79+
)
80+
assert response.get("jsonrpc") == "2.0", (
81+
"WebSocket response should have jsonrpc version 2.0"
82+
)
83+
assert response.get("id") == 1, (
84+
"WebSocket response should have matching request id"
85+
)
86+
87+
logger.info(f"WebSocket connection successful: {arkiv_node.ws_url}")
88+
logger.info(f"Chain ID response: {response.get('result', 'N/A')}")
89+
90+
91+
class TestContainerizedNode:
92+
"""Tests specific to containerized (local Docker) nodes."""
93+
94+
def test_node_container_properties(self, arkiv_container: ArkivNode) -> None:
95+
"""Test that containerized nodes expose container properties."""
96+
# Should have access to container
97+
assert arkiv_container.container is not None
98+
assert arkiv_container.http_port > 0
99+
assert arkiv_container.ws_port > 0
100+
101+
def test_node_container_stop_start(self, arkiv_container: ArkivNode) -> None:
102+
"""Test that containerized nodes can be stopped and started."""
103+
# Should be running initially
104+
assert arkiv_container.is_running()
105+
106+
# Stop the node
107+
arkiv_container.stop()
108+
assert not arkiv_container.is_running()
109+
with pytest.raises(
110+
RuntimeError,
111+
match=r"Node is not running\. Call start\(\) first or use context manager\.",
112+
):
113+
_ = arkiv_container.http_url
114+
115+
with pytest.raises(
116+
RuntimeError,
117+
match=r"Node is not running\. Call start\(\) first or use context manager\.",
118+
):
119+
_ = arkiv_container.ws_url
120+
121+
with pytest.raises(
122+
RuntimeError,
123+
match=r"Node is not running\. Call start\(\) first or use context manager\.",
124+
):
125+
_ = arkiv_container.http_port
126+
127+
with pytest.raises(
128+
RuntimeError,
129+
match=r"Node is not running\. Call start\(\) first or use context manager\.",
130+
):
131+
_ = arkiv_container.ws_port
132+
133+
# Stop a 2nd time should be no-op
134+
arkiv_container.stop()
135+
assert not arkiv_container.is_running()
136+
137+
# Start the node again
138+
arkiv_container.start()
139+
assert arkiv_container.is_running()
140+
assert arkiv_container.http_port > 0
141+
assert arkiv_container.ws_port > 0
142+
143+
# Start a 2nd time should be no-op
144+
arkiv_container.start()
145+
assert arkiv_container.is_running()
146+
assert arkiv_container.http_port > 0
147+
assert arkiv_container.ws_port > 0
148+
149+
def test_node_container_arkiv_help_command(
150+
self, arkiv_container: ArkivNode
151+
) -> None:
152+
"""Check if the Arkiv node help command is available via container CLI."""
153+
help_command = ["golembase", "account", "help"]
154+
exit_code, output = arkiv_container.container.exec(help_command)
155+
156+
assert exit_code == 0, f"Help command should succeed, got exit code {exit_code}"
157+
assert b"help" in output.lower() or b"usage" in output.lower(), (
158+
"Help output should contain help or usage information"
159+
)
160+
161+
logger.info(
162+
f"Account help command: {help_command}, exit_code: {exit_code}, output:\n{output.decode()}"
163+
)
164+
165+
def test_node_container_fund_account(self, arkiv_container: ArkivNode) -> None:
166+
"""Test that fund_account works for containerized nodes."""
167+
from arkiv.account import NamedAccount
168+
169+
# Create a test account
170+
account = NamedAccount.create("test_fund")
171+
172+
# Should be able to fund it
173+
arkiv_container.fund_account(account)
174+
175+
# No exception means success
176+
logger.info(f"Successfully funded account {account.name}")
177+
178+
def test_node_container_context_manager(self, arkiv_container: ArkivNode) -> None:
179+
"""Test that containerized nodes work with context managers."""
180+
# arkiv_container fixture already uses context manager, just verify it's running
181+
assert arkiv_container.is_running()
182+
assert not arkiv_container.is_external()
183+
184+
185+
class TestExternalNode:
186+
"""Tests specific to external (remote) nodes - uses Kaolin testnet."""
187+
188+
def test_node_external_urls(self, arkiv_testnet: ArkivNode) -> None:
189+
"""Test that external nodes have proper URLs configured."""
190+
191+
# Should have URLs
192+
assert arkiv_testnet.http_url.startswith("http")
193+
assert arkiv_testnet.ws_url.startswith("ws")
194+
195+
logger.info(f"External node HTTP URL: {arkiv_testnet.http_url}")
196+
logger.info(f"External node WS URL: {arkiv_testnet.ws_url}")
197+
198+
def test_node_external_no_container(self, arkiv_testnet: ArkivNode) -> None:
199+
"""Test that external nodes raise errors when accessing container properties."""
200+
201+
# Should raise error when accessing container
202+
with pytest.raises(RuntimeError, match="External nodes do not have containers"):
203+
_ = arkiv_testnet.container
204+
205+
# Should raise error when accessing ports
206+
with pytest.raises(
207+
RuntimeError, match="External nodes do not expose port information"
208+
):
209+
_ = arkiv_testnet.http_port
210+
211+
with pytest.raises(
212+
RuntimeError, match="External nodes do not expose port information"
213+
):
214+
_ = arkiv_testnet.ws_port
215+
216+
def test_node_external_cannot_start(self, arkiv_testnet: ArkivNode) -> None:
217+
"""Test that external nodes cannot be started."""
218+
219+
with pytest.raises(RuntimeError, match="Cannot start external node"):
220+
arkiv_testnet.start()
221+
222+
def test_node_external_cannot_stop(self, arkiv_testnet: ArkivNode) -> None:
223+
"""Test that external nodes cannot be stopped."""
224+
225+
with pytest.raises(RuntimeError, match="Cannot stop external node"):
226+
arkiv_testnet.stop()
227+
228+
def test_node_external_cannot_fund(self, arkiv_testnet: ArkivNode) -> None:
229+
"""Test that external nodes cannot fund accounts."""
230+
231+
from arkiv.account import NamedAccount
232+
233+
account = NamedAccount.create("test_external")
234+
235+
with pytest.raises(RuntimeError, match="Cannot fund account on external node"):
236+
arkiv_testnet.fund_account(account)
237+
238+
def test_node_external_context_manager(self, arkiv_testnet: ArkivNode) -> None:
239+
"""Test that external nodes work safely with context managers."""
240+
241+
# Should work without errors (no-op)
242+
with arkiv_testnet as node:
243+
assert node.is_running()
244+
assert node.http_url == arkiv_testnet.http_url
245+
246+
# Node should still be running after context exit
247+
assert arkiv_testnet.is_running()
248+
249+
def test_node_external_is_external(self, arkiv_testnet: ArkivNode) -> None:
250+
"""Verify that external nodes are properly identified."""
251+
252+
assert arkiv_testnet.is_external()

0 commit comments

Comments
 (0)