mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 10:05:29 -05:00
1990 lines
65 KiB
Python
1990 lines
65 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
from dataclasses import dataclass, field, replace
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, Tuple, Type, Union, cast
|
|
|
|
import aiohttp
|
|
import pkg_resources
|
|
import pytest
|
|
from aiohttp.web_ws import WebSocketResponse
|
|
from pytest_mock import MockerFixture
|
|
|
|
from chia.daemon.client import connect_to_daemon
|
|
from chia.daemon.keychain_server import (
|
|
DeleteLabelRequest,
|
|
GetKeyRequest,
|
|
GetKeyResponse,
|
|
GetKeysResponse,
|
|
GetPublicKeyResponse,
|
|
GetPublicKeysResponse,
|
|
SetLabelRequest,
|
|
)
|
|
from chia.daemon.server import WebSocketServer, plotter_log_path, service_plotter
|
|
from chia.plotters.plotters import call_plotters
|
|
from chia.server.outbound_message import NodeType
|
|
from chia.simulator.block_tools import BlockTools
|
|
from chia.simulator.keyring import TempKeyring
|
|
from chia.simulator.time_out_assert import time_out_assert, time_out_assert_custom_interval
|
|
from chia.types.peer_info import PeerInfo
|
|
from chia.util.config import load_config
|
|
from chia.util.ints import uint16
|
|
from chia.util.json_util import dict_to_json_str
|
|
from chia.util.keychain import Keychain, KeyData, supports_os_passphrase_storage
|
|
from chia.util.keyring_wrapper import DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE, KeyringWrapper
|
|
from chia.util.ws_message import create_payload, create_payload_dict
|
|
from chia.wallet.derive_keys import master_sk_to_farmer_sk, master_sk_to_pool_sk
|
|
from tests.core.node_height import node_height_at_least
|
|
from tests.util.misc import Marks, datacases
|
|
|
|
chiapos_version = pkg_resources.get_distribution("chiapos").version
|
|
|
|
|
|
@dataclass
|
|
class RouteCase:
|
|
route: str
|
|
description: str
|
|
request: Dict[str, Any]
|
|
response: Dict[str, Any]
|
|
marks: Marks = ()
|
|
|
|
@property
|
|
def id(self) -> str:
|
|
return f"{self.route}: {self.description}"
|
|
|
|
|
|
@dataclass
|
|
class WalletAddressCase:
|
|
id: str
|
|
request: Dict[str, Any]
|
|
response: Dict[str, Any]
|
|
pubkeys_only: bool = field(default=False)
|
|
marks: Marks = ()
|
|
|
|
|
|
@dataclass
|
|
class KeysForPlotCase:
|
|
id: str
|
|
request: Dict[str, Any]
|
|
response: Dict[str, Any]
|
|
marks: Marks = ()
|
|
|
|
|
|
@dataclass
|
|
class ChiaPlottersBladebitArgsCase:
|
|
case_id: str
|
|
plot_type: str
|
|
count: int = 1
|
|
threads: int = 0
|
|
pool_contract: str = "txch1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
compress: int = 1
|
|
device: int = 0
|
|
hybrid_disk_mode: Optional[int] = None
|
|
farmer_pk: str = ""
|
|
final_dir: str = ""
|
|
marks: Marks = ()
|
|
|
|
@property
|
|
def id(self) -> str:
|
|
return self.case_id
|
|
|
|
def to_command_array(self) -> List[str]:
|
|
command: List[str] = ["bladebit", self.plot_type]
|
|
command += ["-r", str(self.threads)]
|
|
command += ["-n", str(self.count)]
|
|
command += ["-c", self.pool_contract]
|
|
command += ["-f", self.farmer_pk]
|
|
command += ["--compress", str(self.compress)]
|
|
if self.plot_type == "cudaplot":
|
|
command += ["--device", str(self.device)]
|
|
if self.hybrid_disk_mode is not None:
|
|
command += [f"--disk-{self.hybrid_disk_mode}"]
|
|
command += ["-d", str(self.final_dir)]
|
|
|
|
return command
|
|
|
|
def expected_raw_command_args(self):
|
|
raw_args = []
|
|
raw_args += [
|
|
"--threads",
|
|
str(self.threads),
|
|
"--count",
|
|
str(self.count),
|
|
"--farmer-key",
|
|
str(self.farmer_pk),
|
|
"--pool-contract",
|
|
str(self.pool_contract),
|
|
]
|
|
# --compress is "1" by default
|
|
raw_args += ["--compress", str(self.compress) if self.compress is not None else "1"]
|
|
raw_args += [self.plot_type]
|
|
if self.plot_type == "cudaplot":
|
|
# --device is "0" by default
|
|
raw_args += ["--device", str(self.device) if self.device is not None else "0"]
|
|
if self.hybrid_disk_mode is not None:
|
|
raw_args += [f"--disk-{self.hybrid_disk_mode}"]
|
|
raw_args += [str(self.final_dir)]
|
|
return raw_args
|
|
|
|
|
|
# Simple class that responds to a poll() call used by WebSocketServer.is_running()
|
|
@dataclass
|
|
class Service:
|
|
running: bool
|
|
|
|
def poll(self) -> Optional[int]:
|
|
return None if self.running else 1
|
|
|
|
|
|
# Mock daemon server that forwards to WebSocketServer
|
|
@dataclass
|
|
class Daemon:
|
|
# Instance variables used by WebSocketServer.is_running()
|
|
services: Dict[str, Union[List[Service], Service]]
|
|
connections: Dict[str, Optional[List[Any]]]
|
|
|
|
# Instance variables used by WebSocketServer.get_wallet_addresses()
|
|
net_config: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
def get_command_mapping(self) -> Dict[str, Any]:
|
|
return {
|
|
"get_routes": None,
|
|
"example_one": None,
|
|
"example_two": None,
|
|
"example_three": None,
|
|
}
|
|
|
|
def is_service_running(self, service_name: str) -> bool:
|
|
return WebSocketServer.is_service_running(cast(WebSocketServer, self), service_name)
|
|
|
|
async def running_services(self) -> Dict[str, Any]:
|
|
return await WebSocketServer.running_services(cast(WebSocketServer, self))
|
|
|
|
async def is_running(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
return await WebSocketServer.is_running(cast(WebSocketServer, self), request)
|
|
|
|
async def get_routes(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
return await WebSocketServer.get_routes(
|
|
cast(WebSocketServer, self), websocket=WebSocketResponse(), request=request
|
|
)
|
|
|
|
async def get_wallet_addresses(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
return await WebSocketServer.get_wallet_addresses(
|
|
cast(WebSocketServer, self), websocket=WebSocketResponse(), request=request
|
|
)
|
|
|
|
async def get_keys_for_plotting(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
return await WebSocketServer.get_keys_for_plotting(
|
|
cast(WebSocketServer, self), websocket=WebSocketResponse(), request=request
|
|
)
|
|
|
|
|
|
test_key_data = KeyData.from_mnemonic(
|
|
"grief lock ketchup video day owner torch young work "
|
|
"another venue evidence spread season bright private "
|
|
"tomato remind jaguar original blur embody project can"
|
|
)
|
|
test_key_data_no_secrets = replace(test_key_data, secrets=None)
|
|
|
|
test_key_data_2 = KeyData.from_mnemonic(
|
|
"banana boat fragile ghost fortune beyond aerobic access "
|
|
"hammer stable page grunt venture purse canyon discover "
|
|
"egg vivid spare immune awake code announce message"
|
|
)
|
|
|
|
success_response_data = {
|
|
"success": True,
|
|
}
|
|
|
|
plotter_request_ref = {
|
|
"service": "chia_plotter",
|
|
"plotter": "chiapos",
|
|
"k": 25,
|
|
"r": 2,
|
|
"u": 128,
|
|
"e": True,
|
|
"parallel": False,
|
|
"n": 1,
|
|
"queue": "default",
|
|
"d": "unknown",
|
|
"t": "unknown",
|
|
"t2": "",
|
|
"f": "",
|
|
"plotNFTContractAddr": "",
|
|
"x": True,
|
|
"b": 512,
|
|
"overrideK": True,
|
|
"delay": 0,
|
|
"a": 3598820529,
|
|
"c": "xxx",
|
|
}
|
|
|
|
|
|
def add_private_key_response_data(fingerprint: int) -> Dict[str, object]:
|
|
return {
|
|
"success": True,
|
|
"fingerprint": fingerprint,
|
|
}
|
|
|
|
|
|
def fingerprint_missing_response_data(request_type: Type[object]) -> Dict[str, object]:
|
|
return {
|
|
"success": False,
|
|
"error": "malformed request",
|
|
"error_details": {"message": f"1 field missing for {request_type.__name__}: fingerprint"},
|
|
}
|
|
|
|
|
|
def fingerprint_not_found_response_data(fingerprint: int) -> Dict[str, object]:
|
|
return {
|
|
"success": False,
|
|
"error": "key not found",
|
|
"error_details": {
|
|
"fingerprint": fingerprint,
|
|
},
|
|
}
|
|
|
|
|
|
def get_key_response_data(key: KeyData) -> Dict[str, object]:
|
|
return {"success": True, **GetKeyResponse(key=key).to_json_dict()}
|
|
|
|
|
|
def get_keys_response_data(keys: List[KeyData]) -> Dict[str, object]:
|
|
return {"success": True, **GetKeysResponse(keys=keys).to_json_dict()}
|
|
|
|
|
|
def get_public_key_response_data(key: KeyData) -> Dict[str, object]:
|
|
return {"success": True, **GetPublicKeyResponse(key=key).to_json_dict()}
|
|
|
|
|
|
def get_public_keys_response_data(keys: List[KeyData]) -> Dict[str, object]:
|
|
return {"success": True, **GetPublicKeysResponse(keys=keys).to_json_dict()}
|
|
|
|
|
|
def label_missing_response_data(request_type: Type[Any]) -> Dict[str, Any]:
|
|
return {
|
|
"success": False,
|
|
"error": "malformed request",
|
|
"error_details": {"message": f"1 field missing for {request_type.__name__}: label"},
|
|
}
|
|
|
|
|
|
def label_exists_response_data(fingerprint: int, label: str) -> Dict[str, Any]:
|
|
return {
|
|
"success": False,
|
|
"error": "malformed request",
|
|
"error_details": {"message": f"label {label!r} already exists for fingerprint {str(fingerprint)!r}"},
|
|
}
|
|
|
|
|
|
label_empty_response_data = {
|
|
"success": False,
|
|
"error": "malformed request",
|
|
"error_details": {"message": "label can't be empty or whitespace only"},
|
|
}
|
|
|
|
label_too_long_response_data = {
|
|
"success": False,
|
|
"error": "malformed request",
|
|
"error_details": {"message": "label exceeds max length: 66/65"},
|
|
}
|
|
|
|
label_newline_or_tab_response_data = {
|
|
"success": False,
|
|
"error": "malformed request",
|
|
"error_details": {"message": "label can't contain newline or tab"},
|
|
}
|
|
|
|
|
|
def assert_response(
|
|
response: aiohttp.http_websocket.WSMessage, expected_response_data: Dict[str, Any], request_id: Optional[str] = None
|
|
) -> None:
|
|
# Expect: JSON response
|
|
assert response.type == aiohttp.WSMsgType.TEXT
|
|
message = json.loads(response.data.strip())
|
|
# Expect: daemon handled the request
|
|
assert message["ack"] is True
|
|
if request_id is not None:
|
|
assert message["request_id"] == request_id
|
|
# Expect: data matches the expected data
|
|
assert message["data"] == expected_response_data
|
|
|
|
|
|
def assert_response_success_only(response: aiohttp.http_websocket.WSMessage, request_id: Optional[str] = None) -> None:
|
|
# Expect: JSON response
|
|
assert response.type == aiohttp.WSMsgType.TEXT
|
|
message = json.loads(response.data.strip())
|
|
# Expect: {"success": True}
|
|
if request_id is not None:
|
|
assert message["request_id"] == request_id
|
|
assert message["data"]["success"] is True
|
|
|
|
|
|
def assert_running_services_response(response_dict: Dict[str, Any], expected_response_dict: Dict[str, Any]) -> None:
|
|
for k, v in expected_response_dict.items():
|
|
if k == "running_services":
|
|
# Order of services is not guaranteed
|
|
assert len(response_dict[k]) == len(v)
|
|
assert set(response_dict[k]) == set(v)
|
|
else:
|
|
assert response_dict[k] == v
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def mock_lonely_daemon():
|
|
# Mock daemon server without any registered services/connections
|
|
return Daemon(services={}, connections={}, net_config={})
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def mock_daemon_with_services():
|
|
# Mock daemon server with a couple running services, a plotter, and one stopped service
|
|
return Daemon(
|
|
services={
|
|
"my_refrigerator": [Service(True)],
|
|
"the_river": [Service(True)],
|
|
"your_nose": [Service(False)],
|
|
"chia_plotter": [Service(True), Service(True)],
|
|
},
|
|
connections={},
|
|
net_config={},
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def mock_daemon_with_services_and_connections():
|
|
# Mock daemon server with a couple running services, a plotter, and a couple active connections
|
|
return Daemon(
|
|
services={
|
|
"my_refrigerator": [Service(True)],
|
|
"chia_plotter": [Service(True), Service(True)],
|
|
"apple": [Service(True)],
|
|
},
|
|
connections={
|
|
"apple": [1],
|
|
"banana": [1, 2],
|
|
},
|
|
net_config={},
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def get_keychain_for_function():
|
|
with TempKeyring() as keychain:
|
|
yield keychain
|
|
KeyringWrapper.cleanup_shared_instance()
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def mock_daemon_with_config_and_keys(get_keychain_for_function, root_path_populated_with_config):
|
|
root_path = root_path_populated_with_config
|
|
config = load_config(root_path, "config.yaml")
|
|
keychain = Keychain()
|
|
|
|
# populate the keychain with some test keys
|
|
keychain.add_private_key(test_key_data.mnemonic_str())
|
|
keychain.add_private_key(test_key_data_2.mnemonic_str())
|
|
|
|
# Mock daemon server with net_config set for mainnet
|
|
return Daemon(services={}, connections={}, net_config=config)
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
async def daemon_client_with_config_and_keys(get_keychain_for_function, get_daemon, bt):
|
|
keychain = Keychain()
|
|
|
|
# populate the keychain with some test keys
|
|
keychain.add_private_key(test_key_data.mnemonic_str())
|
|
keychain.add_private_key(test_key_data_2.mnemonic_str())
|
|
|
|
daemon = get_daemon
|
|
client = await connect_to_daemon(
|
|
daemon.self_hostname,
|
|
daemon.daemon_port,
|
|
50 * 1000 * 1000,
|
|
bt.get_daemon_ssl_context(),
|
|
heartbeat=daemon.heartbeat,
|
|
)
|
|
return client
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_daemon_simulation(self_hostname, daemon_simulation):
|
|
deamon_and_nodes, get_b_tools, bt = daemon_simulation
|
|
node1, node2, _, _, _, _, _, _, _, _, daemon1 = deamon_and_nodes
|
|
server1 = node1.full_node.server
|
|
node2_port = node2.full_node.server.get_port()
|
|
await server1.start_client(PeerInfo(self_hostname, uint16(node2_port)))
|
|
|
|
async def num_connections():
|
|
count = len(node2.server.get_connections(NodeType.FULL_NODE))
|
|
return count
|
|
|
|
await time_out_assert_custom_interval(60, 1, num_connections, 1)
|
|
|
|
await time_out_assert(1500, node_height_at_least, True, node2, 1)
|
|
|
|
session = aiohttp.ClientSession()
|
|
|
|
log = logging.getLogger()
|
|
log.warning(f"Connecting to daemon on port {daemon1.daemon_port}")
|
|
ws = await session.ws_connect(
|
|
f"wss://127.0.0.1:{daemon1.daemon_port}",
|
|
autoclose=True,
|
|
autoping=True,
|
|
ssl_context=get_b_tools.get_daemon_ssl_context(),
|
|
max_msg_size=100 * 1024 * 1024,
|
|
)
|
|
service_name = "test_service_name"
|
|
data = {"service": service_name}
|
|
payload = create_payload("register_service", data, service_name, "daemon")
|
|
await ws.send_str(payload)
|
|
message_queue: asyncio.Queue = asyncio.Queue()
|
|
|
|
async def reader(ws, queue):
|
|
while True:
|
|
# ClientWebSocketReponse::receive() internally handles PING, PONG, and CLOSE messages
|
|
msg = await ws.receive()
|
|
if msg.type == aiohttp.WSMsgType.TEXT:
|
|
message = msg.data.strip()
|
|
message = json.loads(message)
|
|
await queue.put(message)
|
|
else:
|
|
if msg.type == aiohttp.WSMsgType.ERROR:
|
|
await ws.close()
|
|
elif msg.type == aiohttp.WSMsgType.CLOSED:
|
|
pass
|
|
|
|
break
|
|
|
|
read_handler = asyncio.create_task(reader(ws, message_queue))
|
|
data = {}
|
|
payload = create_payload("get_blockchain_state", data, service_name, "chia_full_node")
|
|
await ws.send_str(payload)
|
|
|
|
await asyncio.sleep(5)
|
|
blockchain_state_found = False
|
|
while not message_queue.empty():
|
|
message = await message_queue.get()
|
|
if message["command"] == "get_blockchain_state":
|
|
blockchain_state_found = True
|
|
|
|
await ws.close()
|
|
read_handler.cancel()
|
|
assert blockchain_state_found
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"service, expected_result",
|
|
[
|
|
(
|
|
"my_refrigerator",
|
|
False,
|
|
),
|
|
(
|
|
service_plotter,
|
|
False,
|
|
),
|
|
],
|
|
)
|
|
def test_is_service_running_no_services(mock_lonely_daemon, service, expected_result):
|
|
daemon = mock_lonely_daemon
|
|
assert daemon.is_service_running(service) == expected_result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"service, expected_result",
|
|
[
|
|
(
|
|
"my_refrigerator",
|
|
True,
|
|
),
|
|
(
|
|
service_plotter,
|
|
True,
|
|
),
|
|
(
|
|
"your_nose",
|
|
False,
|
|
),
|
|
(
|
|
"the_river",
|
|
True,
|
|
),
|
|
(
|
|
"the_clock",
|
|
False,
|
|
),
|
|
],
|
|
)
|
|
def test_is_service_running_with_services(mock_daemon_with_services, service, expected_result):
|
|
daemon = mock_daemon_with_services
|
|
assert daemon.is_service_running(service) == expected_result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"service, expected_result",
|
|
[
|
|
(
|
|
"my_refrigerator",
|
|
True,
|
|
),
|
|
(
|
|
service_plotter,
|
|
True,
|
|
),
|
|
(
|
|
"apple",
|
|
True,
|
|
),
|
|
(
|
|
"banana",
|
|
True,
|
|
),
|
|
(
|
|
"orange",
|
|
False,
|
|
),
|
|
],
|
|
)
|
|
def test_is_service_running_with_services_and_connections(
|
|
mock_daemon_with_services_and_connections, service, expected_result
|
|
):
|
|
daemon = mock_daemon_with_services_and_connections
|
|
assert daemon.is_service_running(service) == expected_result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_running_services_no_services(mock_lonely_daemon):
|
|
daemon = mock_lonely_daemon
|
|
response = await daemon.running_services()
|
|
assert_running_services_response(response, {"success": True, "running_services": []})
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_running_services_with_services(mock_daemon_with_services):
|
|
daemon = mock_daemon_with_services
|
|
response = await daemon.running_services()
|
|
assert_running_services_response(
|
|
response, {"success": True, "running_services": ["my_refrigerator", "the_river", service_plotter]}
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_running_services_with_services_and_connections(mock_daemon_with_services_and_connections):
|
|
daemon = mock_daemon_with_services_and_connections
|
|
response = await daemon.running_services()
|
|
assert_running_services_response(
|
|
response, {"success": True, "running_services": ["my_refrigerator", "apple", "banana", service_plotter]}
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_routes(mock_lonely_daemon):
|
|
daemon = mock_lonely_daemon
|
|
response = await daemon.get_routes({})
|
|
assert response == {
|
|
"success": True,
|
|
"routes": ["get_routes", "example_one", "example_two", "example_three"],
|
|
}
|
|
|
|
|
|
@datacases(
|
|
WalletAddressCase(
|
|
id="no params",
|
|
request={},
|
|
response={
|
|
"success": True,
|
|
"wallet_addresses": {
|
|
test_key_data.fingerprint: [
|
|
{
|
|
"address": "xch1zze67l3jgxuvyaxhjhu7326sezxxve7lgzvq0497ddggzhff7c9s2pdcwh",
|
|
"hd_path": "m/12381/8444/2/0",
|
|
},
|
|
],
|
|
test_key_data_2.fingerprint: [
|
|
{
|
|
"address": "xch1fra5h0qnsezrxenjyslyxx7y4l268gq52m0rgenh58vn8f577uzswzvk4v",
|
|
"hd_path": "m/12381/8444/2/0",
|
|
}
|
|
],
|
|
},
|
|
},
|
|
),
|
|
WalletAddressCase(
|
|
id="list of fingerprints",
|
|
request={"fingerprints": [test_key_data.fingerprint]},
|
|
response={
|
|
"success": True,
|
|
"wallet_addresses": {
|
|
test_key_data.fingerprint: [
|
|
{
|
|
"address": "xch1zze67l3jgxuvyaxhjhu7326sezxxve7lgzvq0497ddggzhff7c9s2pdcwh",
|
|
"hd_path": "m/12381/8444/2/0",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
),
|
|
WalletAddressCase(
|
|
id="count and index",
|
|
request={"fingerprints": [test_key_data.fingerprint], "count": 2, "index": 1},
|
|
response={
|
|
"success": True,
|
|
"wallet_addresses": {
|
|
test_key_data.fingerprint: [
|
|
{
|
|
"address": "xch16jqcaguq27z8xvpu89j7eaqfzn6k89hdrrlm0rffku85n8n7m7sqqmmahh",
|
|
"hd_path": "m/12381/8444/2/1",
|
|
},
|
|
{
|
|
"address": "xch1955vj0gx5tqe7v5tceajn2p4z4pup8d4g2exs0cz4xjqses8ru6qu8zp3y",
|
|
"hd_path": "m/12381/8444/2/2",
|
|
},
|
|
]
|
|
},
|
|
},
|
|
),
|
|
WalletAddressCase(
|
|
id="hardened derivations",
|
|
request={"fingerprints": [test_key_data.fingerprint], "non_observer_derivation": True},
|
|
response={
|
|
"success": True,
|
|
"wallet_addresses": {
|
|
test_key_data.fingerprint: [
|
|
{
|
|
"address": "xch1k996a7h3agygjhqtrf0ycpa7wfd6k5ye2plkf54ukcmdj44gkqkq880l7n",
|
|
"hd_path": "m/12381n/8444n/2n/0n",
|
|
}
|
|
]
|
|
},
|
|
},
|
|
),
|
|
WalletAddressCase(
|
|
id="invalid fingerprint",
|
|
request={"fingerprints": [999999]},
|
|
response={
|
|
"success": False,
|
|
"error": "key(s) not found for fingerprint(s) {999999}",
|
|
},
|
|
),
|
|
WalletAddressCase(
|
|
id="missing private key",
|
|
request={"fingerprints": [test_key_data.fingerprint]},
|
|
response={
|
|
"success": False,
|
|
"error": f"missing private key for key with fingerprint {test_key_data.fingerprint}",
|
|
},
|
|
pubkeys_only=True,
|
|
),
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_get_wallet_addresses(
|
|
mock_daemon_with_config_and_keys,
|
|
monkeypatch,
|
|
case: WalletAddressCase,
|
|
):
|
|
daemon = mock_daemon_with_config_and_keys
|
|
|
|
original_get_keys = Keychain.get_keys
|
|
|
|
def get_keys_no_secrets(self, include_secrets):
|
|
return original_get_keys(self, include_secrets=False)
|
|
|
|
# in the pubkeys_only case, we're ensuring that only pubkeys are returned by get_keys,
|
|
# which will have the effect of causing get_wallet_addresses to raise an exception
|
|
if case.pubkeys_only:
|
|
# monkeypatch Keychain.get_keys() to always call get_keys() with include_secrets=False
|
|
monkeypatch.setattr(Keychain, "get_keys", get_keys_no_secrets)
|
|
|
|
assert case.response == await daemon.get_wallet_addresses(case.request)
|
|
|
|
|
|
@datacases(
|
|
KeysForPlotCase(
|
|
id="no params",
|
|
# When not specifying exact fingerprints, `get_keys_for_plotting` returns
|
|
# all farmer_pk/pool_pk data for available fingerprints
|
|
request={},
|
|
response={
|
|
"success": True,
|
|
"keys": {
|
|
test_key_data.fingerprint: {
|
|
"farmer_public_key": bytes(master_sk_to_farmer_sk(test_key_data.private_key).get_g1()).hex(),
|
|
"pool_public_key": bytes(master_sk_to_pool_sk(test_key_data.private_key).get_g1()).hex(),
|
|
},
|
|
test_key_data_2.fingerprint: {
|
|
"farmer_public_key": bytes(master_sk_to_farmer_sk(test_key_data_2.private_key).get_g1()).hex(),
|
|
"pool_public_key": bytes(master_sk_to_pool_sk(test_key_data_2.private_key).get_g1()).hex(),
|
|
},
|
|
},
|
|
},
|
|
),
|
|
KeysForPlotCase(
|
|
id="list of fingerprints",
|
|
request={"fingerprints": [test_key_data.fingerprint]},
|
|
response={
|
|
"success": True,
|
|
"keys": {
|
|
test_key_data.fingerprint: {
|
|
"farmer_public_key": bytes(master_sk_to_farmer_sk(test_key_data.private_key).get_g1()).hex(),
|
|
"pool_public_key": bytes(master_sk_to_pool_sk(test_key_data.private_key).get_g1()).hex(),
|
|
},
|
|
},
|
|
},
|
|
),
|
|
KeysForPlotCase(
|
|
id="invalid fingerprint",
|
|
request={"fingerprints": [999999]},
|
|
response={
|
|
"success": False,
|
|
"error": "key(s) not found for fingerprint(s) {999999}",
|
|
},
|
|
),
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_get_keys_for_plotting(
|
|
mock_daemon_with_config_and_keys,
|
|
monkeypatch,
|
|
case: KeysForPlotCase,
|
|
):
|
|
daemon = mock_daemon_with_config_and_keys
|
|
assert case.response == await daemon.get_keys_for_plotting(case.request)
|
|
|
|
|
|
@datacases(
|
|
KeysForPlotCase(
|
|
id="invalid request format",
|
|
request={"fingerprints": test_key_data.fingerprint},
|
|
response={},
|
|
),
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_get_keys_for_plotting_error(
|
|
mock_daemon_with_config_and_keys,
|
|
monkeypatch,
|
|
case: KeysForPlotCase,
|
|
):
|
|
daemon = mock_daemon_with_config_and_keys
|
|
with pytest.raises(ValueError, match="fingerprints must be a list of integer"):
|
|
await daemon.get_keys_for_plotting(case.request)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_keys_for_plotting_client(daemon_client_with_config_and_keys):
|
|
client = await daemon_client_with_config_and_keys
|
|
response = await client.get_keys_for_plotting()
|
|
assert response["data"]["success"] is True
|
|
assert len(response["data"]["keys"]) == 2
|
|
assert str(test_key_data.fingerprint) in response["data"]["keys"]
|
|
assert str(test_key_data_2.fingerprint) in response["data"]["keys"]
|
|
response = await client.get_keys_for_plotting([test_key_data.fingerprint])
|
|
assert response["data"]["success"] is True
|
|
assert len(response["data"]["keys"]) == 1
|
|
assert str(test_key_data.fingerprint) in response["data"]["keys"]
|
|
assert str(test_key_data_2.fingerprint) not in response["data"]["keys"]
|
|
await client.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"service_request, expected_result, expected_exception",
|
|
[
|
|
({}, None, KeyError),
|
|
(
|
|
{"service": "my_refrigerator"},
|
|
{"success": True, "service_name": "my_refrigerator", "is_running": False},
|
|
None,
|
|
),
|
|
],
|
|
)
|
|
async def test_is_running_no_services(mock_lonely_daemon, service_request, expected_result, expected_exception):
|
|
daemon = mock_lonely_daemon
|
|
if expected_exception is not None:
|
|
with pytest.raises(expected_exception):
|
|
await daemon.is_running(service_request)
|
|
else:
|
|
response = await daemon.is_running(service_request)
|
|
assert response == expected_result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"service_request, expected_result, expected_exception",
|
|
[
|
|
({}, None, KeyError),
|
|
(
|
|
{"service": "my_refrigerator"},
|
|
{"success": True, "service_name": "my_refrigerator", "is_running": True},
|
|
None,
|
|
),
|
|
(
|
|
{"service": "your_nose"},
|
|
{"success": True, "service_name": "your_nose", "is_running": False},
|
|
None,
|
|
),
|
|
(
|
|
{"service": "the_river"},
|
|
{"success": True, "service_name": "the_river", "is_running": True},
|
|
None,
|
|
),
|
|
(
|
|
{"service": service_plotter},
|
|
{"success": True, "service_name": service_plotter, "is_running": True},
|
|
None,
|
|
),
|
|
],
|
|
)
|
|
async def test_is_running_with_services(
|
|
mock_daemon_with_services, service_request, expected_result, expected_exception
|
|
):
|
|
daemon = mock_daemon_with_services
|
|
if expected_exception is not None:
|
|
with pytest.raises(expected_exception):
|
|
await daemon.is_running(service_request)
|
|
else:
|
|
response = await daemon.is_running(service_request)
|
|
assert response == expected_result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"service_request, expected_result, expected_exception",
|
|
[
|
|
({}, None, KeyError),
|
|
(
|
|
{"service": "my_refrigerator"},
|
|
{"success": True, "service_name": "my_refrigerator", "is_running": True},
|
|
None,
|
|
),
|
|
(
|
|
{"service": "your_nose"},
|
|
{"success": True, "service_name": "your_nose", "is_running": False},
|
|
None,
|
|
),
|
|
(
|
|
{"service": "apple"},
|
|
{"success": True, "service_name": "apple", "is_running": True},
|
|
None,
|
|
),
|
|
(
|
|
{"service": "banana"},
|
|
{"success": True, "service_name": "banana", "is_running": True},
|
|
None,
|
|
),
|
|
(
|
|
{"service": "orange"},
|
|
{"success": True, "service_name": "orange", "is_running": False},
|
|
None,
|
|
),
|
|
],
|
|
)
|
|
async def test_is_running_with_services_and_connections(
|
|
mock_daemon_with_services_and_connections, service_request, expected_result, expected_exception
|
|
):
|
|
daemon = mock_daemon_with_services_and_connections
|
|
if expected_exception is not None:
|
|
with pytest.raises(expected_exception):
|
|
await daemon.is_running(service_request)
|
|
else:
|
|
response = await daemon.is_running(service_request)
|
|
assert response == expected_result
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_validate_keyring_passphrase_rpc(daemon_connection_and_temp_keychain):
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
# When: the keychain has a master passphrase set
|
|
keychain.set_master_passphrase(
|
|
current_passphrase=DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE, new_passphrase="the correct passphrase"
|
|
)
|
|
|
|
bad_passphrase_case_response_data = {
|
|
"success": False,
|
|
"error": None,
|
|
}
|
|
|
|
missing_passphrase_response_data = {
|
|
"success": False,
|
|
"error": "missing key",
|
|
}
|
|
|
|
empty_passphrase_response_data = {
|
|
"success": False,
|
|
"error": None,
|
|
}
|
|
|
|
# When: using the correct passphrase
|
|
await ws.send_str(
|
|
create_payload("validate_keyring_passphrase", {"key": "the correct passphrase"}, "test", "daemon")
|
|
)
|
|
# Expect: validation succeeds
|
|
# TODO: unify error responses in the server, sometimes we add `error: None` sometimes not.
|
|
assert_response(await ws.receive(), {**success_response_data, "error": None})
|
|
|
|
# When: using the wrong passphrase
|
|
await ws.send_str(create_payload("validate_keyring_passphrase", {"key": "the wrong passphrase"}, "test", "daemon"))
|
|
# Expect: validation failure
|
|
assert_response(await ws.receive(), bad_passphrase_case_response_data)
|
|
|
|
# When: not including the passphrase in the payload
|
|
await ws.send_str(create_payload("validate_keyring_passphrase", {}, "test", "daemon"))
|
|
# Expect: validation failure
|
|
assert_response(await ws.receive(), missing_passphrase_response_data)
|
|
|
|
# When: including an empty passphrase in the payload
|
|
await ws.send_str(create_payload("validate_keyring_passphrase", {"key": ""}, "test", "daemon"))
|
|
# Expect: validation failure
|
|
assert_response(await ws.receive(), empty_passphrase_response_data)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_private_key(daemon_connection_and_temp_keychain):
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
mnemonic_with_typo = f"{test_key_data.mnemonic_str()}xyz" # intentional typo: can -> canxyz
|
|
mnemonic_with_missing_word = " ".join(test_key_data.mnemonic_str()[:-1]) # missing last word
|
|
|
|
missing_mnemonic_response_data = {
|
|
"success": False,
|
|
"error": "malformed request",
|
|
"error_details": {"message": "missing mnemonic"},
|
|
}
|
|
|
|
mnemonic_with_typo_response_data = {
|
|
"success": False,
|
|
"error": "'canxyz' is not in the mnemonic dictionary; may be misspelled",
|
|
}
|
|
|
|
invalid_mnemonic_length_response_data = {
|
|
"success": False,
|
|
"error": "Invalid mnemonic length",
|
|
}
|
|
|
|
invalid_mnemonic_response_data = {
|
|
"success": False,
|
|
"error": "Invalid order of mnemonic words",
|
|
}
|
|
|
|
# Expect the key hasn't been added yet
|
|
assert keychain.get_private_key_by_fingerprint(test_key_data.fingerprint) is None
|
|
|
|
await ws.send_str(create_payload("add_private_key", {"mnemonic": test_key_data.mnemonic_str()}, "test", "daemon"))
|
|
# Expect: key was added successfully
|
|
assert_response(await ws.receive(), add_private_key_response_data(test_key_data.fingerprint))
|
|
|
|
# When: missing mnemonic
|
|
await ws.send_str(create_payload("add_private_key", {}, "test", "daemon"))
|
|
# Expect: Failure due to missing mnemonic
|
|
assert_response(await ws.receive(), missing_mnemonic_response_data)
|
|
|
|
# When: using a mmnemonic with an incorrect word (typo)
|
|
await ws.send_str(create_payload("add_private_key", {"mnemonic": mnemonic_with_typo}, "test", "daemon"))
|
|
# Expect: Failure due to misspelled mnemonic
|
|
assert_response(await ws.receive(), mnemonic_with_typo_response_data)
|
|
|
|
# When: using a mnemonic with an incorrect word count
|
|
await ws.send_str(create_payload("add_private_key", {"mnemonic": mnemonic_with_missing_word}, "test", "daemon"))
|
|
# Expect: Failure due to invalid mnemonic
|
|
assert_response(await ws.receive(), invalid_mnemonic_length_response_data)
|
|
|
|
# When: using an incorrect mnemnonic
|
|
await ws.send_str(create_payload("add_private_key", {"mnemonic": " ".join(["abandon"] * 24)}, "test", "daemon"))
|
|
# Expect: Failure due to checksum error
|
|
assert_response(await ws.receive(), invalid_mnemonic_response_data)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_private_key_label(daemon_connection_and_temp_keychain):
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
async def assert_add_private_key_with_label(
|
|
key_data: KeyData, request: Dict[str, object], add_private_key_response: Dict[str, object]
|
|
) -> None:
|
|
await ws.send_str(create_payload("add_private_key", request, "test", "daemon"))
|
|
assert_response(await ws.receive(), add_private_key_response)
|
|
await ws.send_str(
|
|
create_payload("get_key", {"fingerprint": key_data.fingerprint, "include_secrets": True}, "test", "daemon")
|
|
)
|
|
assert_response(await ws.receive(), get_key_response_data(key_data))
|
|
|
|
# without `label` parameter
|
|
key_data_0 = KeyData.generate()
|
|
await assert_add_private_key_with_label(
|
|
key_data_0,
|
|
{"mnemonic": key_data_0.mnemonic_str()},
|
|
add_private_key_response_data(key_data_0.fingerprint),
|
|
)
|
|
# with `label=None`
|
|
key_data_1 = KeyData.generate()
|
|
await assert_add_private_key_with_label(
|
|
key_data_1,
|
|
{"mnemonic": key_data_1.mnemonic_str(), "label": None},
|
|
add_private_key_response_data(key_data_1.fingerprint),
|
|
)
|
|
# with `label="key_2"`
|
|
key_data_2 = KeyData.generate("key_2")
|
|
await assert_add_private_key_with_label(
|
|
key_data_1,
|
|
{"mnemonic": key_data_2.mnemonic_str(), "label": key_data_2.label},
|
|
add_private_key_response_data(key_data_2.fingerprint),
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_key(daemon_connection_and_temp_keychain):
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
await ws.send_str(create_payload("get_key", {"fingerprint": test_key_data.fingerprint}, "test", "daemon"))
|
|
assert_response(await ws.receive(), fingerprint_not_found_response_data(test_key_data.fingerprint))
|
|
|
|
keychain.add_private_key(test_key_data.mnemonic_str())
|
|
|
|
# without `include_secrets`
|
|
await ws.send_str(create_payload("get_key", {"fingerprint": test_key_data.fingerprint}, "test", "daemon"))
|
|
assert_response(await ws.receive(), get_key_response_data(test_key_data_no_secrets))
|
|
|
|
# with `include_secrets=False`
|
|
await ws.send_str(
|
|
create_payload(
|
|
"get_key", {"fingerprint": test_key_data.fingerprint, "include_secrets": False}, "test", "daemon"
|
|
)
|
|
)
|
|
assert_response(await ws.receive(), get_key_response_data(test_key_data_no_secrets))
|
|
|
|
# with `include_secrets=True`
|
|
await ws.send_str(
|
|
create_payload("get_key", {"fingerprint": test_key_data.fingerprint, "include_secrets": True}, "test", "daemon")
|
|
)
|
|
assert_response(await ws.receive(), get_key_response_data(test_key_data))
|
|
|
|
await ws.send_str(create_payload("get_key", {}, "test", "daemon"))
|
|
assert_response(await ws.receive(), fingerprint_missing_response_data(GetKeyRequest))
|
|
|
|
await ws.send_str(create_payload("get_key", {"fingerprint": 123456}, "test", "daemon"))
|
|
assert_response(await ws.receive(), fingerprint_not_found_response_data(123456))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_keys(daemon_connection_and_temp_keychain):
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
# empty keychain
|
|
await ws.send_str(create_payload("get_keys", {}, "test", "daemon"))
|
|
assert_response(await ws.receive(), get_keys_response_data([]))
|
|
|
|
keys = [KeyData.generate() for _ in range(5)]
|
|
keys_added = []
|
|
for key_data in keys:
|
|
keychain.add_private_key(key_data.mnemonic_str())
|
|
keys_added.append(key_data)
|
|
|
|
get_keys_response_data_without_secrets = get_keys_response_data(
|
|
[replace(key, secrets=None) for key in keys_added]
|
|
)
|
|
|
|
# without `include_secrets`
|
|
await ws.send_str(create_payload("get_keys", {}, "test", "daemon"))
|
|
assert_response(await ws.receive(), get_keys_response_data_without_secrets)
|
|
|
|
# with `include_secrets=False`
|
|
await ws.send_str(create_payload("get_keys", {"include_secrets": False}, "test", "daemon"))
|
|
assert_response(await ws.receive(), get_keys_response_data_without_secrets)
|
|
|
|
# with `include_secrets=True`
|
|
await ws.send_str(create_payload("get_keys", {"include_secrets": True}, "test", "daemon"))
|
|
assert_response(await ws.receive(), get_keys_response_data(keys_added))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_public_key(daemon_connection_and_temp_keychain):
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
# empty keychain
|
|
await ws.send_str(create_payload("get_public_key", {"fingerprint": test_key_data.fingerprint}, "test", "daemon"))
|
|
assert_response(await ws.receive(), fingerprint_not_found_response_data(test_key_data.fingerprint))
|
|
|
|
keychain.add_private_key(test_key_data.mnemonic_str())
|
|
|
|
await ws.send_str(create_payload("get_public_key", {"fingerprint": test_key_data.fingerprint}, "test", "daemon"))
|
|
response = await ws.receive()
|
|
assert_response(response, get_public_key_response_data(test_key_data))
|
|
|
|
# Only allowed_keys are allowed in the key dict
|
|
key_dict = json.loads(response.data)["data"]["key"]
|
|
keys_in_response = [key for key in key_dict.keys()]
|
|
allowed_keys = ["fingerprint", "public_key", "label"]
|
|
for key in keys_in_response:
|
|
assert key in allowed_keys, f"Unexpected key '{key}' found in response."
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_public_keys(daemon_connection_and_temp_keychain):
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
# empty keychain
|
|
await ws.send_str(create_payload("get_public_keys", {}, "test", "daemon"))
|
|
assert_response(await ws.receive(), get_public_keys_response_data([]))
|
|
|
|
# populate keychain
|
|
keys = [KeyData.generate() for _ in range(5)]
|
|
keys_added = []
|
|
for key_data in keys:
|
|
keychain.add_private_key(key_data.mnemonic_str())
|
|
keys_added.append(key_data)
|
|
|
|
get_public_keys_response = get_public_keys_response_data(keys_added)
|
|
await ws.send_str(create_payload("get_public_keys", {}, "test", "daemon"))
|
|
response = await ws.receive()
|
|
assert_response(response, get_public_keys_response)
|
|
|
|
# Only allowed_keys are allowed in the key dict
|
|
allowed_keys = ["fingerprint", "public_key", "label"]
|
|
keys_array = json.loads(response.data)["data"]["keys"]
|
|
for key_dict in keys_array:
|
|
keys_in_response = [key for key in key_dict.keys()]
|
|
for key in keys_in_response:
|
|
assert key in allowed_keys, f"Unexpected key '{key}' found in response."
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_key_renaming(daemon_connection_and_temp_keychain):
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
keychain.add_private_key(test_key_data.mnemonic_str())
|
|
# Rename the key three times
|
|
for i in range(3):
|
|
key_data = replace(test_key_data_no_secrets, label=f"renaming_{i}")
|
|
await ws.send_str(
|
|
create_payload(
|
|
"set_label", {"fingerprint": key_data.fingerprint, "label": key_data.label}, "test", "daemon"
|
|
)
|
|
)
|
|
assert_response(await ws.receive(), success_response_data)
|
|
|
|
await ws.send_str(create_payload("get_key", {"fingerprint": key_data.fingerprint}, "test", "daemon"))
|
|
assert_response(
|
|
await ws.receive(),
|
|
{
|
|
"success": True,
|
|
"key": key_data.to_json_dict(),
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_key_label_deletion(daemon_connection_and_temp_keychain):
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
keychain.add_private_key(test_key_data.mnemonic_str(), "key_0")
|
|
assert keychain.get_key(test_key_data.fingerprint).label == "key_0"
|
|
await ws.send_str(create_payload("delete_label", {"fingerprint": test_key_data.fingerprint}, "test", "daemon"))
|
|
assert_response(await ws.receive(), success_response_data)
|
|
assert keychain.get_key(test_key_data.fingerprint).label is None
|
|
await ws.send_str(create_payload("delete_label", {"fingerprint": test_key_data.fingerprint}, "test", "daemon"))
|
|
assert_response(await ws.receive(), fingerprint_not_found_response_data(test_key_data.fingerprint))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"method, parameter, response_data_dict",
|
|
[
|
|
(
|
|
"set_label",
|
|
{"fingerprint": test_key_data.fingerprint, "label": "new_label"},
|
|
success_response_data,
|
|
),
|
|
(
|
|
"set_label",
|
|
{"label": "new_label"},
|
|
fingerprint_missing_response_data(SetLabelRequest),
|
|
),
|
|
(
|
|
"set_label",
|
|
{"fingerprint": test_key_data.fingerprint},
|
|
label_missing_response_data(SetLabelRequest),
|
|
),
|
|
(
|
|
"set_label",
|
|
{"fingerprint": test_key_data.fingerprint, "label": ""},
|
|
label_empty_response_data,
|
|
),
|
|
(
|
|
"set_label",
|
|
{"fingerprint": test_key_data.fingerprint, "label": "a" * 66},
|
|
label_too_long_response_data,
|
|
),
|
|
(
|
|
"set_label",
|
|
{"fingerprint": test_key_data.fingerprint, "label": "a\nb"},
|
|
label_newline_or_tab_response_data,
|
|
),
|
|
(
|
|
"set_label",
|
|
{"fingerprint": test_key_data.fingerprint, "label": "a\tb"},
|
|
label_newline_or_tab_response_data,
|
|
),
|
|
(
|
|
"set_label",
|
|
{"fingerprint": test_key_data.fingerprint, "label": "key_0"},
|
|
label_exists_response_data(test_key_data.fingerprint, "key_0"),
|
|
),
|
|
(
|
|
"delete_label",
|
|
{"fingerprint": test_key_data.fingerprint},
|
|
success_response_data,
|
|
),
|
|
(
|
|
"delete_label",
|
|
{},
|
|
fingerprint_missing_response_data(DeleteLabelRequest),
|
|
),
|
|
(
|
|
"delete_label",
|
|
{"fingerprint": 123456},
|
|
fingerprint_not_found_response_data(123456),
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_key_label_methods(
|
|
daemon_connection_and_temp_keychain, method: str, parameter: Dict[str, Any], response_data_dict: Dict[str, Any]
|
|
) -> None:
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
keychain.add_private_key(test_key_data.mnemonic_str(), "key_0")
|
|
await ws.send_str(create_payload(method, parameter, "test", "daemon"))
|
|
assert_response(await ws.receive(), response_data_dict)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bad_json(daemon_connection_and_temp_keychain: Tuple[aiohttp.ClientWebSocketResponse, Keychain]) -> None:
|
|
ws, _ = daemon_connection_and_temp_keychain
|
|
|
|
await ws.send_str("{doo: '12'}") # send some bad json
|
|
response = await ws.receive()
|
|
|
|
# check for error response
|
|
assert response.type == aiohttp.WSMsgType.TEXT
|
|
message = json.loads(response.data.strip())
|
|
assert message["data"]["success"] is False
|
|
assert message["data"]["error"].startswith("Expecting property name")
|
|
|
|
# properly register a service
|
|
service_name = "test_service"
|
|
data = {"service": service_name}
|
|
payload = create_payload("register_service", data, service_name, "daemon")
|
|
await ws.send_str(payload)
|
|
await ws.receive()
|
|
|
|
# send some more bad json
|
|
await ws.send_str("{doo: '12'}") # send some bad json
|
|
response = await ws.receive()
|
|
assert response.type == aiohttp.WSMsgType.TEXT
|
|
message = json.loads(response.data.strip())
|
|
assert message["command"] != "register_service"
|
|
assert message["data"]["success"] is False
|
|
assert message["data"]["error"].startswith("Expecting property name")
|
|
|
|
|
|
@datacases(
|
|
RouteCase(
|
|
route="register_service",
|
|
description="no service name",
|
|
request={
|
|
"fred": "barney",
|
|
},
|
|
response={"success": False},
|
|
),
|
|
RouteCase(
|
|
route="register_service",
|
|
description="chia_plotter",
|
|
request={
|
|
"service": "chia_plotter",
|
|
},
|
|
response={"success": True, "service": "chia_plotter", "queue": []},
|
|
),
|
|
RouteCase(
|
|
route="unknown_command",
|
|
description="non-existant route",
|
|
request={},
|
|
response={"success": False, "error": "unknown_command unknown_command"},
|
|
),
|
|
RouteCase(
|
|
route="running_services",
|
|
description="successful",
|
|
request={},
|
|
response={"success": True, "running_services": []},
|
|
),
|
|
RouteCase(
|
|
route="keyring_status",
|
|
description="successful",
|
|
request={},
|
|
response={
|
|
"can_save_passphrase": supports_os_passphrase_storage(),
|
|
"can_set_passphrase_hint": True,
|
|
"is_keyring_locked": False,
|
|
"passphrase_hint": "",
|
|
"passphrase_requirements": {"is_optional": True, "min_length": 8},
|
|
"success": True,
|
|
"user_passphrase_is_set": False,
|
|
},
|
|
),
|
|
RouteCase(
|
|
route="get_status",
|
|
description="successful",
|
|
request={},
|
|
response={"success": True, "genesis_initialized": True},
|
|
),
|
|
RouteCase(
|
|
route="get_plotters",
|
|
description="successful",
|
|
request={},
|
|
response={
|
|
"success": True,
|
|
"plotters": {
|
|
"bladebit": {
|
|
"can_install": True,
|
|
"cuda_support": False,
|
|
"display_name": "BladeBit Plotter",
|
|
"installed": False,
|
|
},
|
|
"chiapos": {"display_name": "Chia Proof of Space", "installed": True, "version": chiapos_version},
|
|
"madmax": {"can_install": True, "display_name": "madMAx Plotter", "installed": False},
|
|
},
|
|
},
|
|
),
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_misc_daemon_ws(
|
|
daemon_connection_and_temp_keychain: Tuple[aiohttp.ClientWebSocketResponse, Keychain],
|
|
case: RouteCase,
|
|
) -> None:
|
|
ws, _ = daemon_connection_and_temp_keychain
|
|
|
|
payload = create_payload(case.route, case.request, "service_name", "daemon")
|
|
await ws.send_str(payload)
|
|
response = await ws.receive()
|
|
|
|
assert_response(response, case.response)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unexpected_json(
|
|
daemon_connection_and_temp_keychain: Tuple[aiohttp.ClientWebSocketResponse, Keychain]
|
|
) -> None:
|
|
ws, _ = daemon_connection_and_temp_keychain
|
|
|
|
await ws.send_str('{"this": "is valid but not expected"}') # send some valid but unexpected json
|
|
response = await ws.receive()
|
|
|
|
# check for error response
|
|
assert response.type == aiohttp.WSMsgType.TEXT
|
|
message = json.loads(response.data.strip())
|
|
assert message["data"]["success"] is False
|
|
assert message["data"]["error"].startswith("'command'")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"command_to_test",
|
|
[("start_service"), ("stop_service"), ("start_plotting"), ("stop_plotting"), ("is_running"), ("register_service")],
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_commands_with_no_data(
|
|
daemon_connection_and_temp_keychain: Tuple[aiohttp.ClientWebSocketResponse, Keychain], command_to_test: str
|
|
) -> None:
|
|
ws, _ = daemon_connection_and_temp_keychain
|
|
|
|
payload = create_payload(command_to_test, {}, "service_name", "daemon")
|
|
|
|
await ws.send_str(payload)
|
|
response = await ws.receive()
|
|
|
|
assert_response(response, {"success": False, "error": f'{command_to_test} requires "data"'})
|
|
|
|
|
|
@datacases(
|
|
RouteCase(
|
|
route="set_keyring_passphrase",
|
|
description="no passphrase",
|
|
request={
|
|
"passphrase_hint": "this is a hint",
|
|
"save_passphrase": False,
|
|
},
|
|
response={"success": False, "error": "missing new_passphrase"},
|
|
),
|
|
RouteCase(
|
|
route="set_keyring_passphrase",
|
|
description="incorrect type",
|
|
request={
|
|
"passphrase_hint": "this is a hint",
|
|
"save_passphrase": False,
|
|
"new_passphrase": True,
|
|
},
|
|
response={"success": False, "error": "missing new_passphrase"},
|
|
),
|
|
RouteCase(
|
|
route="set_keyring_passphrase",
|
|
description="correct",
|
|
request={
|
|
"passphrase_hint": "this is a hint",
|
|
"new_passphrase": "this is a passphrase",
|
|
},
|
|
response={"success": True, "error": None},
|
|
),
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_set_keyring_passphrase_ws(
|
|
daemon_connection_and_temp_keychain: Tuple[aiohttp.ClientWebSocketResponse, Keychain],
|
|
case: RouteCase,
|
|
) -> None:
|
|
ws, _ = daemon_connection_and_temp_keychain
|
|
|
|
payload = create_payload(case.route, case.request, "service_name", "daemon")
|
|
await ws.send_str(payload)
|
|
response = await ws.receive()
|
|
|
|
assert_response(response, case.response)
|
|
|
|
|
|
@datacases(
|
|
RouteCase(
|
|
route="remove_keyring_passphrase",
|
|
description="wrong current passphrase",
|
|
request={"current_passphrase": "wrong passphrase"},
|
|
response={"success": False, "error": "current passphrase is invalid"},
|
|
),
|
|
RouteCase(
|
|
route="remove_keyring_passphrase",
|
|
description="incorrect type",
|
|
request={"current_passphrase": True},
|
|
response={"success": False, "error": "missing current_passphrase"},
|
|
),
|
|
RouteCase(
|
|
route="remove_keyring_passphrase",
|
|
description="missing current passphrase",
|
|
request={},
|
|
response={"success": False, "error": "missing current_passphrase"},
|
|
),
|
|
RouteCase(
|
|
route="remove_keyring_passphrase",
|
|
description="correct",
|
|
request={"current_passphrase": "this is a passphrase"},
|
|
response={"success": True, "error": None},
|
|
),
|
|
RouteCase(
|
|
route="unlock_keyring",
|
|
description="wrong current passphrase",
|
|
request={"key": "wrong passphrase"},
|
|
response={"success": False, "error": "bad passphrase"},
|
|
),
|
|
RouteCase(
|
|
route="unlock_keyring",
|
|
description="incorrect type",
|
|
request={"key": True},
|
|
response={"success": False, "error": "missing key"},
|
|
),
|
|
RouteCase(
|
|
route="unlock_keyring",
|
|
description="missing data",
|
|
request={},
|
|
response={"success": False, "error": "missing key"},
|
|
),
|
|
RouteCase(
|
|
route="unlock_keyring",
|
|
description="correct",
|
|
request={"key": "this is a passphrase"},
|
|
response={"success": True, "error": None},
|
|
),
|
|
RouteCase(
|
|
route="set_keyring_passphrase",
|
|
description="no current passphrase",
|
|
request={
|
|
"save_passphrase": False,
|
|
"new_passphrase": "another new passphrase",
|
|
},
|
|
response={"success": False, "error": "missing current_passphrase"},
|
|
),
|
|
RouteCase(
|
|
route="set_keyring_passphrase",
|
|
description="incorrect current passphrase",
|
|
request={
|
|
"save_passphrase": False,
|
|
"current_passphrase": "none",
|
|
"new_passphrase": "another new passphrase",
|
|
},
|
|
response={"success": False, "error": "current passphrase is invalid"},
|
|
),
|
|
RouteCase(
|
|
route="set_keyring_passphrase",
|
|
description="incorrect type",
|
|
request={
|
|
"save_passphrase": False,
|
|
"current_passphrase": False,
|
|
"new_passphrase": "another new passphrase",
|
|
},
|
|
response={"success": False, "error": "missing current_passphrase"},
|
|
),
|
|
RouteCase(
|
|
route="set_keyring_passphrase",
|
|
description="correct",
|
|
request={
|
|
"save_passphrase": False,
|
|
"current_passphrase": "this is a passphrase",
|
|
"new_passphrase": "another new passphrase",
|
|
},
|
|
response={"success": True, "error": None},
|
|
),
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_passphrase_apis(
|
|
daemon_connection_and_temp_keychain: Tuple[aiohttp.ClientWebSocketResponse, Keychain],
|
|
case: RouteCase,
|
|
) -> None:
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
keychain.set_master_passphrase(
|
|
current_passphrase=DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE, new_passphrase="this is a passphrase"
|
|
)
|
|
|
|
payload = create_payload(
|
|
case.route,
|
|
case.request,
|
|
"service_name",
|
|
"daemon",
|
|
)
|
|
await ws.send_str(payload)
|
|
response = await ws.receive()
|
|
|
|
assert_response(response, case.response)
|
|
|
|
|
|
@datacases(
|
|
RouteCase(
|
|
route="unlock_keyring",
|
|
description="exception",
|
|
request={"key": "this is a passphrase"},
|
|
response={"success": False, "error": "validation exception"},
|
|
),
|
|
RouteCase(
|
|
route="validate_keyring_passphrase",
|
|
description="exception",
|
|
request={"key": "this is a passphrase"},
|
|
response={"success": False, "error": "validation exception"},
|
|
),
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_keyring_file_deleted(
|
|
daemon_connection_and_temp_keychain: Tuple[aiohttp.ClientWebSocketResponse, Keychain],
|
|
case: RouteCase,
|
|
) -> None:
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
keychain.set_master_passphrase(
|
|
current_passphrase=DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE, new_passphrase="this is a passphrase"
|
|
)
|
|
keychain.keyring_wrapper.keyring.keyring_path.unlink()
|
|
|
|
payload = create_payload(
|
|
case.route,
|
|
case.request,
|
|
"service_name",
|
|
"daemon",
|
|
)
|
|
await ws.send_str(payload)
|
|
response = await ws.receive()
|
|
|
|
assert_response(response, case.response)
|
|
|
|
|
|
@datacases(
|
|
RouteCase(
|
|
route="start_plotting",
|
|
description="chiapos - missing k",
|
|
request={k: v for k, v in plotter_request_ref.items() if k != "k"},
|
|
response={"success": False, "error": "'k'"},
|
|
),
|
|
RouteCase(
|
|
route="start_plotting",
|
|
description="chiapos - missing d",
|
|
request={k: v for k, v in plotter_request_ref.items() if k != "d"},
|
|
response={"success": False, "error": "'d'"},
|
|
),
|
|
RouteCase(
|
|
route="start_plotting",
|
|
description="chiapos - missing t",
|
|
request={k: v for k, v in plotter_request_ref.items() if k != "t"},
|
|
response={"success": False, "error": "'t'"},
|
|
),
|
|
RouteCase(
|
|
route="start_plotting",
|
|
description="chiapos - both c and p",
|
|
request={
|
|
**plotter_request_ref,
|
|
"c": "hello",
|
|
"p": "goodbye",
|
|
},
|
|
response={
|
|
"success": False,
|
|
"service_name": "chia_plotter",
|
|
"error": "Choose one of pool_contract_address and pool_public_key",
|
|
},
|
|
),
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_plotter_errors(
|
|
daemon_connection_and_temp_keychain: Tuple[aiohttp.ClientWebSocketResponse, Keychain], case: RouteCase
|
|
) -> None:
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
payload = create_payload(
|
|
case.route,
|
|
case.request,
|
|
"test_service_name",
|
|
"daemon",
|
|
)
|
|
await ws.send_str(payload)
|
|
response = await ws.receive()
|
|
|
|
assert_response(response, case.response)
|
|
|
|
|
|
@datacases(
|
|
RouteCase(
|
|
route="start_plotting",
|
|
description="bladebit - ramplot",
|
|
request={
|
|
**plotter_request_ref,
|
|
"plotter": "bladebit",
|
|
"plot_type": "ramplot",
|
|
"w": True,
|
|
"m": True,
|
|
"no_cpu_affinity": True,
|
|
"e": False,
|
|
},
|
|
response={
|
|
"success": True,
|
|
},
|
|
),
|
|
RouteCase(
|
|
route="start_plotting",
|
|
description="bladebit - diskplot",
|
|
request={
|
|
**plotter_request_ref,
|
|
"plotter": "bladebit",
|
|
"plot_type": "diskplot",
|
|
"w": True,
|
|
"m": True,
|
|
"no_cpu_affinity": True,
|
|
"e": False,
|
|
"cache": "cache",
|
|
"f1_threads": 5,
|
|
"fp_threads": 6,
|
|
"c_threads": 4,
|
|
"p2_threads": 4,
|
|
"p3_threads": 4,
|
|
"alternate": True,
|
|
"no_t1_direct": True,
|
|
"no_t2_direct": True,
|
|
},
|
|
response={
|
|
"success": True,
|
|
},
|
|
),
|
|
RouteCase(
|
|
route="start_plotting",
|
|
description="bladebit - cudaplot - hybrid 128 mode",
|
|
request={
|
|
**plotter_request_ref,
|
|
"plotter": "bladebit",
|
|
"plot_type": "cudaplot",
|
|
"w": True,
|
|
"m": True,
|
|
"no_cpu_affinity": True,
|
|
"e": False,
|
|
"compress": 1,
|
|
"disk_128": True,
|
|
},
|
|
response={
|
|
"success": True,
|
|
},
|
|
),
|
|
RouteCase(
|
|
route="start_plotting",
|
|
description="bladebit - cudaplot - hybrid 16 mode",
|
|
request={
|
|
**plotter_request_ref,
|
|
"plotter": "bladebit",
|
|
"plot_type": "cudaplot",
|
|
"w": True,
|
|
"m": True,
|
|
"no_cpu_affinity": True,
|
|
"e": False,
|
|
"compress": 1,
|
|
"disk_16": True,
|
|
},
|
|
response={
|
|
"success": True,
|
|
},
|
|
),
|
|
RouteCase(
|
|
route="start_plotting",
|
|
description="madmax",
|
|
request={
|
|
**plotter_request_ref,
|
|
"plotter": "madmax",
|
|
"w": True,
|
|
"m": True,
|
|
"no_cpu_affinity": True,
|
|
"t2": "testing",
|
|
"v": 128,
|
|
},
|
|
response={
|
|
"success": True,
|
|
},
|
|
),
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_plotter_options(
|
|
daemon_connection_and_temp_keychain: Tuple[aiohttp.ClientWebSocketResponse, Keychain],
|
|
get_b_tools: BlockTools,
|
|
case: RouteCase,
|
|
) -> None:
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
# register for chia_plotter events
|
|
service_name = "chia_plotter"
|
|
data = {"service": service_name}
|
|
payload = create_payload("register_service", data, "chia_plotter", "daemon")
|
|
await ws.send_str(payload)
|
|
response = await ws.receive()
|
|
assert_response_success_only(response)
|
|
|
|
case.request["t"] = str(get_b_tools.root_path)
|
|
case.request["d"] = str(get_b_tools.root_path)
|
|
|
|
payload_rpc = create_payload_dict(
|
|
case.route,
|
|
case.request,
|
|
"test_service_name",
|
|
"daemon",
|
|
)
|
|
payload = dict_to_json_str(payload_rpc)
|
|
await ws.send_str(payload)
|
|
response = await ws.receive()
|
|
|
|
assert_response_success_only(response, payload_rpc["request_id"])
|
|
|
|
|
|
def assert_plot_queue_response(
|
|
response: aiohttp.http_websocket.WSMessage,
|
|
expected_command: str,
|
|
expected_message_state: str,
|
|
expected_plot_id: str,
|
|
expected_plot_state: str,
|
|
) -> None:
|
|
assert response.type == aiohttp.WSMsgType.TEXT
|
|
message = json.loads(response.data.strip())
|
|
assert message["command"] == expected_command
|
|
assert message["data"]["state"] == expected_message_state
|
|
plot_info = message["data"]["queue"][0]
|
|
assert plot_info["id"] == expected_plot_id
|
|
assert plot_info["state"] == expected_plot_state
|
|
|
|
|
|
def check_plot_queue_log(
|
|
response: aiohttp.http_websocket.WSMessage,
|
|
expected_command: str,
|
|
expected_message_state: str,
|
|
expected_plot_id: str,
|
|
expected_plot_state: str,
|
|
expected_log_entry: str,
|
|
) -> bool:
|
|
assert_plot_queue_response(
|
|
response, expected_command, expected_message_state, expected_plot_id, expected_plot_state
|
|
)
|
|
|
|
message = json.loads(response.data.strip())
|
|
plot_info = message["data"]["queue"][0]
|
|
|
|
return plot_info["log_new"].startswith(expected_log_entry)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_plotter_roundtrip(
|
|
daemon_connection_and_temp_keychain: Tuple[aiohttp.ClientWebSocketResponse, Keychain], get_b_tools: BlockTools
|
|
) -> None:
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
# register for chia_plotter events
|
|
service_name = "chia_plotter"
|
|
data = {"service": service_name}
|
|
payload = create_payload("register_service", data, "chia_plotter", "daemon")
|
|
await ws.send_str(payload)
|
|
response = await ws.receive()
|
|
assert_response_success_only(response)
|
|
|
|
root_path = get_b_tools.root_path
|
|
|
|
plotting_request: Dict[str, Any] = {
|
|
**plotter_request_ref,
|
|
"d": str(root_path),
|
|
"t": str(root_path),
|
|
"p": "xxx",
|
|
}
|
|
plotting_request.pop("c", None)
|
|
|
|
payload_rpc = create_payload_dict(
|
|
"start_plotting",
|
|
plotting_request,
|
|
"test_service_name",
|
|
"daemon",
|
|
)
|
|
payload = dict_to_json_str(payload_rpc)
|
|
await ws.send_str(payload)
|
|
|
|
# should first get response to start_plottin
|
|
response = await ws.receive()
|
|
assert response.type == aiohttp.WSMsgType.TEXT
|
|
message = json.loads(response.data.strip())
|
|
assert message["data"]["success"] is True
|
|
assert message["request_id"] == payload_rpc["request_id"]
|
|
plot_id = message["data"]["ids"][0]
|
|
|
|
# 1) Submitted
|
|
response = await ws.receive()
|
|
assert_plot_queue_response(response, "state_changed", "state_changed", plot_id, "SUBMITTED")
|
|
|
|
# 2) Running
|
|
response = await ws.receive()
|
|
assert_plot_queue_response(response, "state_changed", "state_changed", plot_id, "RUNNING")
|
|
|
|
# Write chiapos magic words to the log file to signal finished
|
|
plot_log_path = plotter_log_path(root_path, plot_id)
|
|
with open(plot_log_path, "a") as f:
|
|
f.write("Renamed final file")
|
|
f.flush()
|
|
|
|
# 3) log_changed
|
|
final_log_entry = False
|
|
while not final_log_entry:
|
|
response = await ws.receive()
|
|
final_log_entry = check_plot_queue_log(
|
|
response, "state_changed", "log_changed", plot_id, "RUNNING", "Renamed final file"
|
|
)
|
|
if not final_log_entry:
|
|
with open(plot_log_path, "a") as f:
|
|
f.write("Renamed final file")
|
|
f.flush()
|
|
|
|
# 4) Finished
|
|
response = await ws.receive()
|
|
assert_plot_queue_response(response, "state_changed", "state_changed", plot_id, "FINISHED")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_plotter_stop_plotting(
|
|
daemon_connection_and_temp_keychain: Tuple[aiohttp.ClientWebSocketResponse, Keychain], get_b_tools: BlockTools
|
|
) -> None:
|
|
ws, keychain = daemon_connection_and_temp_keychain
|
|
|
|
# register for chia_plotter events
|
|
service_name = "chia_plotter"
|
|
data = {"service": service_name}
|
|
payload = create_payload("register_service", data, "chia_plotter", "daemon")
|
|
await ws.send_str(payload)
|
|
response = await ws.receive()
|
|
assert_response_success_only(response)
|
|
|
|
root_path = get_b_tools.root_path
|
|
|
|
plotting_request: Dict[str, Any] = {
|
|
**plotter_request_ref,
|
|
"d": str(root_path),
|
|
"t": str(root_path),
|
|
}
|
|
|
|
payload_rpc = create_payload_dict(
|
|
"start_plotting",
|
|
plotting_request,
|
|
"test_service_name",
|
|
"daemon",
|
|
)
|
|
payload = dict_to_json_str(payload_rpc)
|
|
await ws.send_str(payload)
|
|
|
|
# should first get response to start_plotting
|
|
response = await ws.receive()
|
|
assert response.type == aiohttp.WSMsgType.TEXT
|
|
message = json.loads(response.data.strip())
|
|
assert message["data"]["success"] is True
|
|
# make sure matches the start_plotting request
|
|
assert message["request_id"] == payload_rpc["request_id"]
|
|
plot_id = message["data"]["ids"][0]
|
|
|
|
# 1) Submitted
|
|
response = await ws.receive()
|
|
assert_plot_queue_response(response, "state_changed", "state_changed", plot_id, "SUBMITTED")
|
|
|
|
# 2) Running
|
|
response = await ws.receive()
|
|
assert_plot_queue_response(response, "state_changed", "state_changed", plot_id, "RUNNING")
|
|
|
|
payload_rpc = create_payload_dict(
|
|
"stop_plotting",
|
|
{"id": plot_id},
|
|
"service_name",
|
|
"daemon",
|
|
)
|
|
|
|
stop_plotting_request_id = payload_rpc["request_id"]
|
|
payload = dict_to_json_str(payload_rpc)
|
|
await ws.send_str(payload)
|
|
|
|
# 3) removing
|
|
response = await ws.receive()
|
|
assert_plot_queue_response(response, "state_changed", "state_changed", plot_id, "REMOVING")
|
|
|
|
# 4) Finished
|
|
response = await ws.receive()
|
|
assert_plot_queue_response(response, "state_changed", "state_changed", plot_id, "FINISHED")
|
|
|
|
# 5) Finally, get the "ack" for the stop_plotting payload
|
|
response = await ws.receive()
|
|
assert_response(response, {"success": True}, stop_plotting_request_id)
|
|
|
|
|
|
@datacases(
|
|
ChiaPlottersBladebitArgsCase(case_id="1", plot_type="cudaplot"),
|
|
ChiaPlottersBladebitArgsCase(case_id="2", plot_type="cudaplot", hybrid_disk_mode=16),
|
|
ChiaPlottersBladebitArgsCase(case_id="3", plot_type="cudaplot", hybrid_disk_mode=128),
|
|
)
|
|
def test_run_plotter_bladebit(
|
|
mocker: MockerFixture,
|
|
mock_daemon_with_config_and_keys,
|
|
bt: BlockTools,
|
|
case: ChiaPlottersBladebitArgsCase,
|
|
) -> None:
|
|
root_path = bt.root_path
|
|
|
|
case.farmer_pk = bytes(bt.farmer_pk).hex()
|
|
case.final_dir = str(bt.plot_dir)
|
|
|
|
def bladebit_exists(x: Path) -> bool:
|
|
return True if isinstance(x, Path) and x.parent == root_path / "plotters" else mocker.DEFAULT
|
|
|
|
def get_bladebit_version(_: Path) -> Tuple[bool, List[str]]:
|
|
return True, ["3", "0", "0"]
|
|
|
|
mocker.patch("os.path.exists", side_effect=bladebit_exists)
|
|
mocker.patch("chia.plotters.bladebit.get_bladebit_version", side_effect=get_bladebit_version)
|
|
mock_run_plotter = mocker.patch("chia.plotters.bladebit.run_plotter")
|
|
|
|
call_plotters(root_path, case.to_command_array())
|
|
|
|
assert mock_run_plotter.call_args.args[0] == root_path
|
|
assert mock_run_plotter.call_args.args[1] == "bladebit"
|
|
assert mock_run_plotter.call_args.args[2][1:] == case.expected_raw_command_args()
|
|
mock_run_plotter.assert_called_once()
|