Skip to content

Commit 898f54e

Browse files
author
Matthias Zimmermann
committed
add parallel entity creation testing
1 parent d1865ff commit 898f54e

File tree

9 files changed

+360
-64
lines changed

9 files changed

+360
-64
lines changed

.env.testing

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,43 @@ WS_URL=wss://kaolin.hoodi.arkiv.network/rpc/ws
77

88
# External Wallet Configuration (required when using external node)
99
WALLET_FILE_1=wallet_alice.json
10-
WALLET_PASSWORD_1=s3cret
1110
WALLET_FILE_2=wallet_bob.json
11+
WALLET_FILE_3=wallet_3.json
12+
WALLET_FILE_4=wallet_4.json
13+
WALLET_FILE_5=wallet_5.json
14+
WALLET_FILE_6=wallet_6.json
15+
WALLET_FILE_7=wallet_7.json
16+
WALLET_FILE_8=wallet_8.json
17+
WALLET_FILE_9=wallet_9.json
18+
WALLET_FILE_10=wallet_10.json
19+
WALLET_FILE_11=wallet_11.json
20+
WALLET_FILE_12=wallet_12.json
21+
WALLET_FILE_13=wallet_13.json
22+
WALLET_FILE_14=wallet_14.json
23+
WALLET_FILE_15=wallet_15.json
24+
WALLET_FILE_16=wallet_16.json
25+
WALLET_FILE_17=wallet_17.json
26+
WALLET_FILE_18=wallet_18.json
27+
WALLET_FILE_19=wallet_19.json
28+
WALLET_FILE_20=wallet_20.json
29+
30+
WALLET_PASSWORD_1=s3cret
1231
WALLET_PASSWORD_2=s3cret
32+
WALLET_PASSWORD_3=s3cret
33+
WALLET_PASSWORD_4=s3cret
34+
WALLET_PASSWORD_5=s3cret
35+
WALLET_PASSWORD_6=s3cret
36+
WALLET_PASSWORD_7=s3cret
37+
WALLET_PASSWORD_8=s3cret
38+
WALLET_PASSWORD_9=s3cret
39+
WALLET_PASSWORD_10=s3cret
40+
WALLET_PASSWORD_11=s3cret
41+
WALLET_PASSWORD_12=s3cret
42+
WALLET_PASSWORD_13=s3cret
43+
WALLET_PASSWORD_14=s3cret
44+
WALLET_PASSWORD_15=s3cret
45+
WALLET_PASSWORD_16=s3cret
46+
WALLET_PASSWORD_17=s3cret
47+
WALLET_PASSWORD_18=s3cret
48+
WALLET_PASSWORD_19=s3cret
49+
WALLET_PASSWORD_20=s3cret

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,28 @@ SDK versions are tracked in the following files:
114114
- `pyproject.toml`
115115
- `uv.lock`
116116

117+
### Testing
118+
119+
Pytest is used for unit and integration testing.
120+
```bash
121+
uv run pytest # Run all tests
122+
uv run pytest -k test_create_entity_simple --log-cli-level=INFO # Specific tests via keyword, print at info log level
123+
```
124+
125+
If an `.env` file is present the unit tests are run against the specifice RPC coordinates and test accounts.
126+
An example wallet file is provided in `.env.testing`
127+
Make sure that the specified test accounts are properly funded before running the tests.
128+
129+
Otherwise, the tests are run against a testcontainer containing an Arkiv RPC Node.
130+
Test accounts are created on the fly and using the CLI inside the local RPC Nonde.
131+
132+
Account wallets for such tests can be created via the command shown below.
133+
The provided example creates the wallet file `wallet_alice.json` using the password provided during the execution of the command.
134+
135+
```bash
136+
uv run python uv run python -m arkiv.account alice
137+
```
138+
117139
### Code Quality
118140

119141
This project uses comprehensive unit testing, linting and type checking to maintain high code quality:

src/arkiv/account.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"""Account management for Arkiv client."""
22

3+
import getpass
34
import json
5+
import sys
6+
from pathlib import Path
47
from typing import Any
58

69
from eth_account import Account
@@ -147,3 +150,38 @@ def _check_and_trim(self, name: str) -> str:
147150
if name is None or len(name.strip()) == 0:
148151
raise AccountNameException("Account name must be a non-empty string.")
149152
return name.strip()
153+
154+
155+
def main() -> None:
156+
import argparse
157+
158+
parser = argparse.ArgumentParser(
159+
description="Create a new named account and write a JSON wallet file."
160+
)
161+
parser.add_argument("name", help="Name of the account (used in wallet_<name>.json)")
162+
args = parser.parse_args()
163+
164+
# Sanitize name for filename (alphanumeric, dash, underscore only)
165+
import re
166+
167+
account_name = re.sub(r"[^a-zA-Z0-9_-]", "_", args.name.strip())
168+
wallet_path = Path(f"wallet_{account_name}.json")
169+
170+
if wallet_path.exists():
171+
print(f'File "{wallet_path}" already exists. Aborting.')
172+
sys.exit(1)
173+
174+
account = NamedAccount(account_name, Account.create())
175+
password = getpass.getpass("Enter wallet password: ")
176+
encrypted = account.local_account.encrypt(password)
177+
178+
with wallet_path.open("w") as f:
179+
json.dump(encrypted, f)
180+
181+
print(f"Named account: {account}")
182+
print(f"Wallet file: {wallet_path}")
183+
184+
185+
# add main entry point
186+
if __name__ == "__main__":
187+
main()

src/arkiv/module.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ def __init__(self, client: "Arkiv") -> None:
4343
# Attach custom Arkiv RPC methods to the eth object
4444
self.client.eth.attach_methods(FUNCTIONS_ABI)
4545
for method_name in FUNCTIONS_ABI.keys():
46-
logger.info(f"Custom RPC method: eth.{method_name}")
46+
logger.debug(f"Custom RPC method: eth.{method_name}")
4747

4848
# Create contract instance for events (using EVENTS_ABI)
4949
self.contract = client.eth.contract(address=STORAGE_ADDRESS, abi=EVENTS_ABI)
5050
for event in self.contract.all_events():
51-
logger.info(f"Entity event {event.topic}: {event.signature}")
51+
logger.debug(f"Entity event {event.topic}: {event.signature}")
5252

5353
def is_available(self) -> bool:
5454
"""Check if Arkiv functionality is available."""
@@ -167,12 +167,12 @@ def _get_storage_value(self, entity_key: EntityKey) -> bytes:
167167
"""Get the storage value stored in the given entity."""
168168
# EntityKey is automatically converted by arkiv_munger
169169
storage_value = base64.b64decode(self.client.eth.get_storage_value(entity_key)) # type: ignore[attr-defined]
170-
logger.info(f"Storage value (decoded): {storage_value!r}")
170+
logger.debug(f"Storage value (decoded): {storage_value!r}")
171171
return storage_value
172172

173173
def _get_entity_metadata(self, entity_key: EntityKey) -> dict[str, Any]:
174174
"""Get the metadata of the given entity."""
175175
# EntityKey is automatically converted by arkiv_munger
176176
metadata: dict[str, Any] = self.client.eth.get_entity_metadata(entity_key) # type: ignore[attr-defined]
177-
logger.info(f"Raw metadata: {metadata}")
177+
logger.debug(f"Raw metadata: {metadata}")
178178
return metadata

tests/conftest.py

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515

1616
from arkiv import Arkiv
1717
from arkiv.account import NamedAccount
18+
from tests.node_container import create_container_node
1819

19-
from .node_container import create_container_node
20+
from .utils import create_account
2021

2122
# Environment variable names for external node configuration
2223
RPC_URL_ENV = "RPC_URL"
@@ -132,65 +133,11 @@ def arkiv_client_http(
132133
def account_1(arkiv_node: tuple[DockerContainer | None, str, str]) -> NamedAccount:
133134
"""Provide the first (alice) account."""
134135
container, _, _ = arkiv_node
135-
return _create_account(1, ALICE, container)
136+
return create_account(1, ALICE, container)
136137

137138

138139
@pytest.fixture(scope="session")
139140
def account_2(arkiv_node: tuple[DockerContainer | None, str, str]) -> NamedAccount:
140141
"""Provide the second (bob) account."""
141142
container, _, _ = arkiv_node
142-
return _create_account(2, BOB, container)
143-
144-
145-
def _create_account(
146-
index: int, name: str, container: DockerContainer | None
147-
) -> NamedAccount:
148-
"""Create a named account from env vars or generate a new one."""
149-
wallet_file = os.getenv(f"{WALLET_FILE_ENV_PREFIX}_{index}")
150-
wallet_password = os.getenv(f"{WALLET_PASSWORD_ENV_PREFIX}_{index}")
151-
152-
if not wallet_file or not wallet_password:
153-
account = NamedAccount.create(name)
154-
if type(container) is DockerContainer:
155-
_fund_account(container, account)
156-
157-
return account
158-
159-
wallet_json = _load_wallet_json(wallet_file)
160-
return NamedAccount.from_wallet(name, wallet_json, wallet_password)
161-
162-
163-
def _load_wallet_json(wallet_file: str) -> str:
164-
"""Load account from encrypted wallet file."""
165-
wallet_path = Path(wallet_file)
166-
167-
if not wallet_path.exists():
168-
raise FileNotFoundError(f"Wallet file not found: {wallet_file}")
169-
170-
with wallet_path.open() as f:
171-
wallet_json = f.read()
172-
173-
return wallet_json
174-
175-
176-
def _fund_account(arkivContainer: DockerContainer, account: NamedAccount) -> None:
177-
"""Fixture to create and fund a test account in the Arkiv node."""
178-
# Get the private key (as hex)
179-
acct_address = account.address
180-
acct_private_key = account.key.hex()
181-
182-
# Import account inside the container
183-
exit_code, output = arkivContainer.exec(
184-
["golembase", "account", "import", "--key", acct_private_key]
185-
)
186-
assert exit_code == 0, f"Account import failed: {output.decode()}"
187-
188-
# Fund account inside the container
189-
exit_code, output = arkivContainer.exec(["golembase", "account", "fund"])
190-
assert exit_code == 0, f"Account funding failed: {output.decode()}"
191-
logger.info(f"Imported and funded account: {acct_address}")
192-
193-
# Printing account balance
194-
exit_code, output = arkivContainer.exec(["golembase", "account", "balance"])
195-
assert exit_code == 0, f"Account balance check failed: {output.decode()}"
196-
logger.info(f"Account balance: {output.decode().strip()}, exit_code: {exit_code}")
143+
return create_account(2, BOB, container)

tests/test_entity_create.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def check_tx_hash(label: str, tx_hash: HexBytes) -> None:
3838
class TestEntityCreate:
3939
"""Test cases for create_entity function."""
4040

41-
def test_create_entity_with_payload_web3(self, arkiv_client_http: Arkiv) -> None:
41+
def test_create_entity_via_web3(self, arkiv_client_http: Arkiv) -> None:
4242
"""Test create_entity with custom payload checking against Web3 client behavior."""
4343
payload = b"Hello world!"
4444
annotations: dict[str, str | int] = {"type": "Greeting", "version": 1}
@@ -150,7 +150,7 @@ def test_create_entity_with_payload_web3(self, arkiv_client_http: Arkiv) -> None
150150
"Entity expiration block should be in the future"
151151
)
152152

153-
def test_create_entity(self, arkiv_client_http: Arkiv) -> None:
153+
def test_create_entity_simple(self, arkiv_client_http: Arkiv) -> None:
154154
"""Test create_entity."""
155155
pl: bytes = b"Hello world!"
156156
ann: dict[str, str | int] = {"type": "Greeting", "version": 1}

0 commit comments

Comments
 (0)