test_: add multiple status-backend instances (#6104)

* test_: add multiple status-backend instances

* test_: reliable schemas
This commit is contained in:
Anton Danchenko 2024-11-21 15:21:53 +01:00 committed by GitHub
parent 11cf42bedd
commit 35dc84fa7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 145 additions and 517 deletions

View File

@ -26,9 +26,12 @@ mkdir -p "${test_results_path}"
all_compose_files="-f ${root_path}/docker-compose.anvil.yml -f ${root_path}/docker-compose.test.status-go.yml"
project_name="status-go-func-tests-$(date +%s)"
export STATUS_BACKEND_COUNT=10
export STATUS_BACKEND_URLS=$(eval echo http://${project_name}-status-backend-{1..${STATUS_BACKEND_COUNT}}:3333 | tr ' ' ,)
# Run functional tests
echo -e "${GRN}Running tests${RST}, HEAD: $(git rev-parse HEAD)"
docker compose -p ${project_name} ${all_compose_files} up -d --build --remove-orphans
docker compose -p ${project_name} ${all_compose_files} up -d --build --scale status-backend=${STATUS_BACKEND_COUNT} --remove-orphans
echo -e "${GRN}Running tests-rpc${RST}" # Follow the logs, wait for them to finish
docker compose -p ${project_name} ${all_compose_files} logs -f tests-rpc > "${root_path}/tests-rpc.log"

View File

@ -25,7 +25,7 @@ Functional tests for status-go
* Status-im contracts will be deployed to the network
### Run tests
- In `./tests-functional` run `docker compose -f docker-compose.anvil.yml -f docker-compose.test.status-go.yml -f docker-compose.status-go.local.yml up --build --remove-orphans`, as result:
- In `./tests-functional` run `docker compose -f docker-compose.anvil.yml -f docker-compose.test.status-go.yml -f docker-compose.status-go.local.yml up --build --scale status-backend=10 --remove-orphans`, as result:
* a container with [status-go as daemon](https://github.com/status-im/status-go/issues/5175) will be created with APIModules exposed on `0.0.0.0:3333`
* status-go will use [anvil](https://book.getfoundry.sh/reference/anvil/) as RPCURL with ChainID 31337
* all Status-im contracts will be deployed to the network

View File

@ -0,0 +1,66 @@
import json
import logging
import jsonschema
import requests
from conftest import option
from json import JSONDecodeError
class RpcClient:
def __init__(self, rpc_url, client=requests.Session()):
self.client = client
self.rpc_url = rpc_url
def _check_decode_and_key_errors_in_response(self, response, key):
try:
return response.json()[key]
except json.JSONDecodeError:
raise AssertionError(
f"Invalid JSON in response: {response.content}")
except KeyError:
raise AssertionError(
f"Key '{key}' not found in the JSON response: {response.content}")
def verify_is_valid_json_rpc_response(self, response, _id=None):
assert response.status_code == 200, f"Got response {response.content}, status code {response.status_code}"
assert response.content
self._check_decode_and_key_errors_in_response(response, "result")
if _id:
try:
if _id != response.json()["id"]:
raise AssertionError(
f"got id: {response.json()['id']} instead of expected id: {_id}"
)
except KeyError:
raise AssertionError(f"no id in response {response.json()}")
return response
def verify_is_json_rpc_error(self, response):
assert response.status_code == 200
assert response.content
self._check_decode_and_key_errors_in_response(response, "error")
def rpc_request(self, method, params=[], request_id=13, url=None):
url = url if url else self.rpc_url
data = {"jsonrpc": "2.0", "method": method, "id": request_id}
if params:
data["params"] = params
logging.info(f"Sending POST request to url {url} with data: {json.dumps(data, sort_keys=True, indent=4)}")
response = self.client.post(url, json=data)
try:
logging.info(f"Got response: {json.dumps(response.json(), sort_keys=True, indent=4)}")
except JSONDecodeError:
logging.info(f"Got response: {response.content}")
return response
def rpc_valid_request(self, method, params=[], _id=None, url=None):
response = self.rpc_request(method, params, _id, url)
self.verify_is_valid_json_rpc_response(response, _id)
return response
def verify_json_schema(self, response, method):
with open(f"{option.base_dir}/schemas/{method}", "r") as schema:
jsonschema.validate(instance=response,
schema=json.load(schema))

View File

@ -1,88 +1,39 @@
import json
import logging
import time
from datetime import datetime
from json import JSONDecodeError
import jsonschema
import random
import threading
import requests
from clients.signals import SignalClient
from clients.rpc import RpcClient
from datetime import datetime
from conftest import option
from constants import user_1
class RpcClient:
def __init__(self, rpc_url, client=requests.Session()):
self.client = client
self.rpc_url = rpc_url
def _check_decode_and_key_errors_in_response(self, response, key):
try:
return response.json()[key]
except json.JSONDecodeError:
raise AssertionError(
f"Invalid JSON in response: {response.content}")
except KeyError:
raise AssertionError(
f"Key '{key}' not found in the JSON response: {response.content}")
def verify_is_valid_json_rpc_response(self, response, _id=None):
assert response.status_code == 200, f"Got response {response.content}, status code {response.status_code}"
assert response.content
self._check_decode_and_key_errors_in_response(response, "result")
if _id:
try:
if _id != response.json()["id"]:
raise AssertionError(
f"got id: {response.json()['id']} instead of expected id: {_id}"
)
except KeyError:
raise AssertionError(f"no id in response {response.json()}")
return response
def verify_is_json_rpc_error(self, response):
assert response.status_code == 200
assert response.content
self._check_decode_and_key_errors_in_response(response, "error")
def rpc_request(self, method, params=[], request_id=13, url=None):
url = url if url else self.rpc_url
data = {"jsonrpc": "2.0", "method": method, "id": request_id}
if params:
data["params"] = params
logging.info(f"Sending POST request to url {url} with data: {json.dumps(data, sort_keys=True, indent=4)}")
response = self.client.post(url, json=data)
try:
logging.info(f"Got response: {json.dumps(response.json(), sort_keys=True, indent=4)}")
except JSONDecodeError:
logging.info(f"Got response: {response.content}")
return response
def rpc_valid_request(self, method, params=[], _id=None, url=None):
response = self.rpc_request(method, params, _id, url)
self.verify_is_valid_json_rpc_response(response, _id)
return response
def verify_json_schema(self, response, method):
with open(f"{option.base_dir}/schemas/{method}", "r") as schema:
jsonschema.validate(instance=response,
schema=json.load(schema))
class StatusBackend(RpcClient, SignalClient):
def __init__(self, await_signals=list()):
def __init__(self, await_signals=[], url=None):
try:
url = url if url else random.choice(option.status_backend_urls)
except IndexError:
raise Exception("Not enough status-backend containers, please add more")
option.status_backend_urls.remove(url)
self.api_url = f"{url}/statusgo"
self.ws_url = f"{url}".replace("http", "ws")
self.rpc_url = f"{url}/statusgo/CallRPC"
self.api_url = f"{option.rpc_url_status_backend}/statusgo"
self.ws_url = f"{option.ws_url_status_backend}"
self.rpc_url = f"{option.rpc_url_status_backend}/statusgo/CallRPC"
RpcClient.__init__(self, self.rpc_url)
SignalClient.__init__(self, self.ws_url, await_signals)
websocket_thread = threading.Thread(target=self._connect)
websocket_thread.daemon = True
websocket_thread.start()
def api_request(self, method, data, url=None):
url = url if url else self.api_url
url = f"{url}/{method}"

View File

@ -12,12 +12,6 @@ def pytest_addoption(parser):
help="",
default="http://0.0.0.0:3333",
)
parser.addoption(
"--rpc_url_status_backend",
action="store",
help="",
default="http://0.0.0.0:3334",
)
parser.addoption(
"--ws_url_statusd",
action="store",
@ -25,10 +19,14 @@ def pytest_addoption(parser):
default="ws://0.0.0.0:8354",
)
parser.addoption(
"--ws_url_status_backend",
"--status_backend_urls",
action="store",
help="",
default="ws://0.0.0.0:3334",
default=[
f"http://0.0.0.0:{3314 + i}" for i in range(
int(os.getenv("STATUS_BACKEND_COUNT", 10))
)
],
)
parser.addoption(
"--anvil_url",
@ -43,7 +41,6 @@ def pytest_addoption(parser):
default="Strong12345",
)
@dataclass
class Option:
pass
@ -55,33 +52,6 @@ option = Option()
def pytest_configure(config):
global option
option = config.option
if type(option.status_backend_urls) is str:
option.status_backend_urls = option.status_backend_urls.split(",")
option.base_dir = os.path.dirname(os.path.abspath(__file__))
@pytest.fixture(scope="session", autouse=True)
def init_status_backend():
await_signals = [
"mediaserver.started",
"node.started",
"node.ready",
"node.login",
"wallet", # TODO: a test per event of a different type
]
from clients.status_backend import StatusBackend
backend_client = StatusBackend(
await_signals=await_signals
)
websocket_thread = threading.Thread(
target=backend_client._connect
)
websocket_thread.daemon = True
websocket_thread.start()
backend_client.init_status_backend()
backend_client.restore_account_and_wait_for_rpc_client_to_start()
yield backend_client

View File

@ -8,4 +8,4 @@ services:
- 8354:8354
status-backend:
ports:
- 3334:3333
- 3314-3324:3333

View File

@ -68,9 +68,8 @@ services:
"-m", "rpc",
"--anvil_url=http://anvil:8545",
"--rpc_url_statusd=http://status-go:3333",
"--rpc_url_status_backend=http://status-backend:3333",
"--status_backend_urls=${STATUS_BACKEND_URLS}",
"--ws_url_statusd=ws://status-go:8354",
"--ws_url_status_backend=ws://status-backend:3333",
"--junitxml=/tests-rpc/reports/report.xml"
]
volumes:

View File

@ -16,154 +16,19 @@
},
"result": {
"type": "object",
"required": [
"DAI",
"ETH",
"EUROC",
"STT",
"UNI",
"USDC",
"WEENUS",
"WETH",
"WETH9",
"XEENUS",
"YEENUS",
"ZEENUS"
],
"properties": {
"DAI": {
"patternProperties": {
"^[a-zA-Z0-9_]+$": {
"type": "object",
"required": [
"usd"
],
"required": ["usd"],
"properties": {
"usd": {
"type": "number"
}
}
},
"ETH": {
"type": "object",
"required": [
"usd"
],
"properties": {
"usd": {
"type": "number"
}
}
},
"EUROC": {
"type": "object",
"required": [
"usd"
],
"properties": {
"usd": {
"type": "integer"
}
}
},
"STT": {
"type": "object",
"required": [
"usd"
],
"properties": {
"usd": {
"type": "number"
}
}
},
"UNI": {
"type": "object",
"required": [
"usd"
],
"properties": {
"usd": {
"type": "number"
}
}
},
"USDC": {
"type": "object",
"required": [
"usd"
],
"properties": {
"usd": {
"type": "number"
}
}
},
"WEENUS": {
"type": "object",
"required": [
"usd"
],
"properties": {
"usd": {
"type": "integer"
}
}
},
"WETH": {
"type": "object",
"required": [
"usd"
],
"properties": {
"usd": {
"type": "number"
}
}
},
"WETH9": {
"type": "object",
"required": [
"usd"
],
"properties": {
"usd": {
"type": "integer"
}
}
},
"XEENUS": {
"type": "object",
"required": [
"usd"
],
"properties": {
"usd": {
"type": "integer"
}
}
},
"YEENUS": {
"type": "object",
"required": [
"usd"
],
"properties": {
"usd": {
"type": "integer"
}
}
},
"ZEENUS": {
"type": "object",
"required": [
"usd"
],
"properties": {
"usd": {
"type": "integer"
}
}
}
}
},
"additionalProperties": false,
"minProperties": 1
}
}
}
}

View File

@ -16,251 +16,8 @@
},
"result": {
"type": "object",
"required": [
"DAI",
"ETH",
"STT",
"UNI",
"USDC",
"WETH"
],
"properties": {
"DAI": {
"type": "object",
"required": [
"Id",
"Name",
"Symbol",
"Description",
"TotalCoinsMined",
"AssetLaunchDate",
"AssetWhitepaperUrl",
"AssetWebsiteUrl",
"BuiltOn",
"SmartContractAddress"
],
"properties": {
"Id": {
"type": "string"
},
"Name": {
"type": "string"
},
"Symbol": {
"type": "string"
},
"Description": {
"type": "string"
},
"TotalCoinsMined": {
"type": "number"
},
"AssetLaunchDate": {
"type": "string"
},
"AssetWhitepaperUrl": {
"type": "string"
},
"AssetWebsiteUrl": {
"type": "string"
},
"BuiltOn": {
"type": "string"
},
"SmartContractAddress": {
"type": "string"
}
}
},
"ETH": {
"type": "object",
"required": [
"Id",
"Name",
"Symbol",
"Description",
"TotalCoinsMined",
"AssetLaunchDate",
"AssetWhitepaperUrl",
"AssetWebsiteUrl",
"BuiltOn",
"SmartContractAddress"
],
"properties": {
"Id": {
"type": "string"
},
"Name": {
"type": "string"
},
"Symbol": {
"type": "string"
},
"Description": {
"type": "string"
},
"TotalCoinsMined": {
"type": "number"
},
"AssetLaunchDate": {
"type": "string"
},
"AssetWhitepaperUrl": {
"type": "string"
},
"AssetWebsiteUrl": {
"type": "string"
},
"BuiltOn": {
"type": "string"
},
"SmartContractAddress": {
"type": "string"
}
}
},
"STT": {
"type": "object",
"required": [
"Id",
"Name",
"Symbol",
"Description",
"TotalCoinsMined",
"AssetLaunchDate",
"AssetWhitepaperUrl",
"AssetWebsiteUrl",
"BuiltOn",
"SmartContractAddress"
],
"properties": {
"Id": {
"type": "string"
},
"Name": {
"type": "string"
},
"Symbol": {
"type": "string"
},
"Description": {
"type": "string"
},
"TotalCoinsMined": {
"type": "number"
},
"AssetLaunchDate": {
"type": "string"
},
"AssetWhitepaperUrl": {
"type": "string"
},
"AssetWebsiteUrl": {
"type": "string"
},
"BuiltOn": {
"type": "string"
},
"SmartContractAddress": {
"type": "string"
}
}
},
"UNI": {
"type": "object",
"required": [
"Id",
"Name",
"Symbol",
"Description",
"TotalCoinsMined",
"AssetLaunchDate",
"AssetWhitepaperUrl",
"AssetWebsiteUrl",
"BuiltOn",
"SmartContractAddress"
],
"properties": {
"Id": {
"type": "string"
},
"Name": {
"type": "string"
},
"Symbol": {
"type": "string"
},
"Description": {
"type": "string"
},
"TotalCoinsMined": {
"type": "number"
},
"AssetLaunchDate": {
"type": "string"
},
"AssetWhitepaperUrl": {
"type": "string"
},
"AssetWebsiteUrl": {
"type": "string"
},
"BuiltOn": {
"type": "string"
},
"SmartContractAddress": {
"type": "string"
}
}
},
"USDC": {
"type": "object",
"required": [
"Id",
"Name",
"Symbol",
"Description",
"TotalCoinsMined",
"AssetLaunchDate",
"AssetWhitepaperUrl",
"AssetWebsiteUrl",
"BuiltOn",
"SmartContractAddress"
],
"properties": {
"Id": {
"type": "string"
},
"Name": {
"type": "string"
},
"Symbol": {
"type": "string"
},
"Description": {
"type": "string"
},
"TotalCoinsMined": {
"type": "number"
},
"AssetLaunchDate": {
"type": "string"
},
"AssetWhitepaperUrl": {
"type": "string"
},
"AssetWebsiteUrl": {
"type": "string"
},
"BuiltOn": {
"type": "string"
},
"SmartContractAddress": {
"type": "string"
}
}
},
"WETH": {
"patternProperties": {
"^[a-zA-Z0-9_]+$": {
"type": "object",
"required": [
"Id",
@ -307,7 +64,9 @@
}
}
}
}
},
"additionalProperties": false,
"minProperties": 1
}
}
}
}

View File

@ -15,8 +15,8 @@ class TestAccounts(StatusBackendTestCase):
[
("accounts_getAccounts", []),
("accounts_getKeypairs", []),
("accounts_hasPairedDevices", []),
("accounts_remainingAccountCapacity", []),
# ("accounts_hasPairedDevices", []), # randomly crashes app, to be reworked/fixed
# ("accounts_remainingAccountCapacity", []), # randomly crashes app, to be reworked/fixed
("multiaccounts_getIdentityImages", [user_1.private_key]),
],

View File

@ -22,10 +22,18 @@ class StatusDTestCase:
class StatusBackendTestCase:
await_signals = []
await_signals = [
"node.ready"
]
def setup_class(self):
self.rpc_client = StatusBackend(await_signals=self.await_signals)
self.rpc_client.init_status_backend()
self.rpc_client.restore_account_and_login()
self.rpc_client.wait_for_signal("node.ready")
self.network_id = 31337
@ -142,11 +150,3 @@ class SignalTestCase(StatusDTestCase):
websocket_thread = threading.Thread(target=self.signal_client._connect)
websocket_thread.daemon = True
websocket_thread.start()
class SignalBackendTestCase(StatusBackendTestCase):
def setup_method(self):
websocket_thread = threading.Thread(target=self.rpc_client._connect)
websocket_thread.daemon = True
websocket_thread.start()

View File

@ -1,3 +1,4 @@
from test_cases import StatusBackend
import pytest
@ -6,10 +7,20 @@ import pytest
class TestInitialiseApp:
@pytest.mark.init
def test_init_app(self, init_status_backend):
# this test is going to fail on every call except first since status-backend will be already initialized
def test_init_app(self):
await_signals = [
"mediaserver.started",
"node.started",
"node.ready",
"node.login",
]
backend_client = StatusBackend(await_signals)
backend_client.init_status_backend()
backend_client.restore_account_and_login()
backend_client = init_status_backend
assert backend_client is not None
backend_client.verify_json_schema(

View File

@ -4,18 +4,22 @@ import random
import pytest
from constants import user_1
from test_cases import SignalBackendTestCase
from test_cases import StatusBackendTestCase
@pytest.mark.wallet
@pytest.mark.rpc
class TestWalletRpcSignal(SignalBackendTestCase):
await_signals = ["wallet", ]
class TestWalletSignals(StatusBackendTestCase):
def setup_class(self):
self.await_signals.append("wallet")
super().setup_class(self)
def setup_method(self):
super().setup_method()
self.request_id = str(random.randint(1, 8888))
@pytest.mark.skip
def test_wallet_get_owned_collectibles_async(self):
method = "wallet_getOwnedCollectiblesAsync"
params = [0, [self.network_id, ], [user_1.address], None, 0, 25, 1,