status-go/tests-functional/clients/status_backend.py

328 lines
11 KiB
Python

import io
import json
import logging
import tarfile
import tempfile
import time
import random
import threading
import requests
import docker
import docker.errors
import os
from tenacity import retry, stop_after_delay, wait_fixed
from clients.signals import SignalClient
from clients.rpc import RpcClient
from conftest import option
from resources.constants import user_1, DEFAULT_DISPLAY_NAME, USER_DIR
NANOSECONDS_PER_SECOND = 1_000_000_000
class StatusBackend(RpcClient, SignalClient):
container = None
def __init__(self, await_signals=[]):
if option.status_backend_url:
url = option.status_backend_url
else:
self.docker_client = docker.from_env()
host_port = random.choice(option.status_backend_port_range)
self.container = self._start_container(host_port)
url = f"http://127.0.0.1:{host_port}"
option.status_backend_port_range.remove(host_port)
self.base_url = url
self.api_url = f"{url}/statusgo"
self.ws_url = f"{url}".replace("http", "ws")
self.rpc_url = f"{url}/statusgo/CallRPC"
RpcClient.__init__(self, self.rpc_url)
SignalClient.__init__(self, self.ws_url, await_signals)
self.wait_for_healthy()
websocket_thread = threading.Thread(target=self._connect)
websocket_thread.daemon = True
websocket_thread.start()
def _start_container(self, host_port):
docker_project_name = option.docker_project_name
timestamp = int(time.time() * 1000) # Keep in sync with run_functional_tests.sh
image_name = f"{docker_project_name}-status-backend:latest"
container_name = f"{docker_project_name}-status-backend-{timestamp}"
coverage_path = option.codecov_dir if option.codecov_dir else os.path.abspath("./coverage/binary")
container_args = {
"image": image_name,
"detach": True,
"name": container_name,
"labels": {"com.docker.compose.project": docker_project_name},
"entrypoint": [
"status-backend",
"--address",
"0.0.0.0:3333",
],
"ports": {"3333/tcp": host_port},
"environment": {
"GOCOVERDIR": "/coverage/binary",
},
"volumes": {
coverage_path: {
"bind": "/coverage/binary",
"mode": "rw",
}
},
}
if "FUNCTIONAL_TESTS_DOCKER_UID" in os.environ:
container_args["user"] = os.environ["FUNCTIONAL_TESTS_DOCKER_UID"]
container = self.docker_client.containers.run(**container_args)
network = self.docker_client.networks.get(f"{docker_project_name}_default")
network.connect(container)
option.status_backend_containers.append(container.id)
return container
def wait_for_healthy(self, timeout=10):
start_time = time.time()
while time.time() - start_time <= timeout:
try:
self.health(enable_logging=False)
logging.info(f"StatusBackend is healthy after {time.time() - start_time} seconds")
return
except Exception:
time.sleep(0.1)
raise TimeoutError(f"StatusBackend was not healthy after {timeout} seconds")
def health(self, enable_logging=True):
return self.api_request("health", data=[], url=self.base_url, enable_logging=enable_logging)
def api_request(self, method, data, url=None, enable_logging=True):
url = url if url else self.api_url
url = f"{url}/{method}"
if enable_logging:
logging.info(f"Sending POST request to url {url} with data: {json.dumps(data, sort_keys=True, indent=4)}")
response = requests.post(url, json=data)
if enable_logging:
logging.info(f"Got response: {response.content}")
return response
def verify_is_valid_api_response(self, response):
assert response.status_code == 200, f"Got response {response.content}, status code {response.status_code}"
assert response.content
logging.info(f"Got response: {response.content}")
try:
error = response.json()["error"]
assert not error, f"Error: {error}"
except json.JSONDecodeError:
raise AssertionError(f"Invalid JSON in response: {response.content}")
except KeyError:
pass
def api_valid_request(self, method, data, url=None):
response = self.api_request(method, data, url)
self.verify_is_valid_api_response(response)
return response
def init_status_backend(self, data_dir=USER_DIR):
method = "InitializeApplication"
data = {
"dataDir": data_dir,
"logEnabled": True,
"logLevel": "DEBUG",
"apiLogging": True,
}
return self.api_valid_request(method, data)
def _set_proxy_credentials(self, data):
if "STATUS_BUILD_PROXY_USER" not in os.environ:
return data
user = os.environ["STATUS_BUILD_PROXY_USER"]
password = os.environ["STATUS_BUILD_PROXY_PASSWORD"]
data["StatusProxyMarketUser"] = user
data["StatusProxyMarketPassword"] = password
data["StatusProxyBlockchainUser"] = user
data["StatusProxyBlockchainPassword"] = password
data["StatusProxyEnabled"] = True
data["StatusProxyStageName"] = "test"
return data
def extract_data(self, path: str):
if not self.container:
return path
try:
stream, _ = self.container.get_archive(path)
except docker.errors.NotFound:
return None
temp_dir = tempfile.mkdtemp()
tar_bytes = io.BytesIO(b"".join(stream))
with tarfile.open(fileobj=tar_bytes) as tar:
tar.extractall(path=temp_dir)
# If the tar contains a single file, return the path to that file
# Otherwise it's a directory, just return temp_dir.
if len(tar.getmembers()) == 1:
return os.path.join(temp_dir, tar.getmembers()[0].name)
return temp_dir
def create_account_and_login(
self,
data_dir=USER_DIR,
display_name=DEFAULT_DISPLAY_NAME,
password=user_1.password,
):
method = "CreateAccountAndLogin"
data = {
"rootDataDir": data_dir,
"kdfIterations": 256000,
"displayName": display_name,
"password": password,
"customizationColor": "primary",
"logEnabled": True,
"logLevel": "DEBUG",
}
data = self._set_proxy_credentials(data)
return self.api_valid_request(method, data)
def restore_account_and_login(
self,
data_dir=USER_DIR,
display_name=DEFAULT_DISPLAY_NAME,
user=user_1,
network_id=31337,
):
method = "RestoreAccountAndLogin"
data = {
"rootDataDir": data_dir,
"kdfIterations": 256000,
"displayName": display_name,
"password": user.password,
"mnemonic": user.passphrase,
"customizationColor": "blue",
"logEnabled": True,
"logLevel": "DEBUG",
"testNetworksEnabled": False,
"networkId": network_id,
"networksOverride": [
{
"ChainID": network_id,
"ChainName": "Anvil",
"DefaultRPCURL": "http://anvil:8545",
"RPCURL": "http://anvil:8545",
"ShortName": "eth",
"NativeCurrencyName": "Ether",
"NativeCurrencySymbol": "ETH",
"NativeCurrencyDecimals": 18,
"IsTest": False,
"Layer": 1,
"Enabled": True,
}
],
}
data = self._set_proxy_credentials(data)
return self.api_valid_request(method, data)
def login(self, keyUid, user=user_1):
method = "LoginAccount"
data = {
"password": user.password,
"keyUid": keyUid,
"kdfIterations": 256000,
}
data = self._set_proxy_credentials(data)
return self.api_valid_request(method, data)
def logout(self, user=user_1):
method = "Logout"
return self.api_valid_request(method, {})
def restore_account_and_wait_for_rpc_client_to_start(self, timeout=60):
self.restore_account_and_login()
start_time = time.time()
# ToDo: change this part for waiting for `node.login` signal when websockets are migrated to StatusBackend
while time.time() - start_time <= timeout:
try:
self.rpc_valid_request(method="accounts_getKeypairs")
return
except AssertionError:
time.sleep(3)
raise TimeoutError(f"RPC client was not started after {timeout} seconds")
@retry(stop=stop_after_delay(10), wait=wait_fixed(0.5), reraise=True)
def start_messenger(self, params=[]):
method = "wakuext_startMessenger"
response = self.rpc_request(method, params)
json_response = response.json()
if "error" in json_response:
assert json_response["error"]["code"] == -32000
assert json_response["error"]["message"] == "messenger already started"
return
self.verify_is_valid_json_rpc_response(response)
def start_wallet(self, params=[]):
method = "wallet_startWallet"
response = self.rpc_request(method, params)
self.verify_is_valid_json_rpc_response(response)
def get_settings(self, params=[]):
method = "settings_getSettings"
response = self.rpc_request(method, params)
self.verify_is_valid_json_rpc_response(response)
def get_accounts(self, params=[]):
method = "accounts_getAccounts"
response = self.rpc_request(method, params)
self.verify_is_valid_json_rpc_response(response)
return response.json()
def get_pubkey(self, display_name):
response = self.get_accounts()
accounts = response.get("result", [])
for account in accounts:
if account.get("name") == display_name:
return account.get("public-key")
raise ValueError(f"Public key not found for display name: {display_name}")
def send_contact_request(self, contact_id: str, message: str):
method = "wakuext_sendContactRequest"
params = [{"id": contact_id, "message": message}]
response = self.rpc_request(method, params)
self.verify_is_valid_json_rpc_response(response)
return response.json()
def accept_contact_request(self, chat_id: str):
method = "wakuext_acceptContactRequest"
params = [{"id": chat_id}]
response = self.rpc_request(method, params)
self.verify_is_valid_json_rpc_response(response)
return response.json()
def get_contacts(self):
method = "wakuext_contacts"
response = self.rpc_request(method)
self.verify_is_valid_json_rpc_response(response)
return response.json()
def send_message(self, contact_id: str, message: str):
method = "wakuext_sendOneToOneMessage"
params = [{"id": contact_id, "message": message}]
response = self.rpc_request(method, params)
self.verify_is_valid_json_rpc_response(response)
return response.json()