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" 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)" 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 # Run functional tests
echo -e "${GRN}Running tests${RST}, HEAD: $(git rev-parse HEAD)" 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 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" 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 * Status-im contracts will be deployed to the network
### Run tests ### 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` * 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 * 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 * 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 json
import logging import logging
import time import time
from datetime import datetime import random
from json import JSONDecodeError import threading
import jsonschema
import requests import requests
from clients.signals import SignalClient from clients.signals import SignalClient
from clients.rpc import RpcClient
from datetime import datetime
from conftest import option from conftest import option
from constants import user_1 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): 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) RpcClient.__init__(self, self.rpc_url)
SignalClient.__init__(self, self.ws_url, await_signals) 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): def api_request(self, method, data, url=None):
url = url if url else self.api_url url = url if url else self.api_url
url = f"{url}/{method}" url = f"{url}/{method}"

View File

@ -12,12 +12,6 @@ def pytest_addoption(parser):
help="", help="",
default="http://0.0.0.0:3333", 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( parser.addoption(
"--ws_url_statusd", "--ws_url_statusd",
action="store", action="store",
@ -25,10 +19,14 @@ def pytest_addoption(parser):
default="ws://0.0.0.0:8354", default="ws://0.0.0.0:8354",
) )
parser.addoption( parser.addoption(
"--ws_url_status_backend", "--status_backend_urls",
action="store", action="store",
help="", 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( parser.addoption(
"--anvil_url", "--anvil_url",
@ -43,7 +41,6 @@ def pytest_addoption(parser):
default="Strong12345", default="Strong12345",
) )
@dataclass @dataclass
class Option: class Option:
pass pass
@ -55,33 +52,6 @@ option = Option()
def pytest_configure(config): def pytest_configure(config):
global option global option
option = config.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__)) 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 - 8354:8354
status-backend: status-backend:
ports: ports:
- 3334:3333 - 3314-3324:3333

View File

@ -68,9 +68,8 @@ services:
"-m", "rpc", "-m", "rpc",
"--anvil_url=http://anvil:8545", "--anvil_url=http://anvil:8545",
"--rpc_url_statusd=http://status-go:3333", "--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_statusd=ws://status-go:8354",
"--ws_url_status_backend=ws://status-backend:3333",
"--junitxml=/tests-rpc/reports/report.xml" "--junitxml=/tests-rpc/reports/report.xml"
] ]
volumes: volumes:

View File

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

View File

@ -16,251 +16,8 @@
}, },
"result": { "result": {
"type": "object", "type": "object",
"required": [ "patternProperties": {
"DAI", "^[a-zA-Z0-9_]+$": {
"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": {
"type": "object", "type": "object",
"required": [ "required": [
"Id", "Id",
@ -307,7 +64,9 @@
} }
} }
} }
} },
"additionalProperties": false,
"minProperties": 1
} }
} }
} }

View File

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

View File

@ -22,10 +22,18 @@ class StatusDTestCase:
class StatusBackendTestCase: class StatusBackendTestCase:
await_signals = []
await_signals = [
"node.ready"
]
def setup_class(self): def setup_class(self):
self.rpc_client = StatusBackend(await_signals=self.await_signals) 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 self.network_id = 31337
@ -142,11 +150,3 @@ class SignalTestCase(StatusDTestCase):
websocket_thread = threading.Thread(target=self.signal_client._connect) websocket_thread = threading.Thread(target=self.signal_client._connect)
websocket_thread.daemon = True websocket_thread.daemon = True
websocket_thread.start() 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 import pytest
@ -6,10 +7,20 @@ import pytest
class TestInitialiseApp: class TestInitialiseApp:
@pytest.mark.init @pytest.mark.init
def test_init_app(self, init_status_backend): def test_init_app(self):
# this test is going to fail on every call except first since status-backend will be already initialized
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 assert backend_client is not None
backend_client.verify_json_schema( backend_client.verify_json_schema(

View File

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