test_: one to one messages (#6119)

* test_: one to one messages

* test_: use default display name

* test_: fix f-string format

* test_: fix signal log save

* test_: put signal saving under flag

* test_: addressed review comments

* test_: address review comment
This commit is contained in:
fbarbu15 2024-12-03 10:11:26 +02:00 committed by GitHub
parent cffd2cfefb
commit ec90b2f4ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 257 additions and 32 deletions

4
.gitignore vendored
View File

@ -107,7 +107,11 @@ __pycache__/
report/results.xml report/results.xml
tests-functional/coverage tests-functional/coverage
tests-functional/reports tests-functional/reports
tests-functional/signals
tests-functional/*.log tests-functional/*.log
pyrightconfig.json
.venv
# generated files # generated files
mock mock

View File

@ -2,7 +2,7 @@ import json
import logging import logging
import jsonschema import jsonschema
import requests import requests
from tenacity import retry, stop_after_delay, wait_fixed
from conftest import option from conftest import option
from json import JSONDecodeError from json import JSONDecodeError
@ -42,6 +42,7 @@ class RpcClient:
assert response.content assert response.content
self._check_decode_and_key_errors_in_response(response, "error") self._check_decode_and_key_errors_in_response(response, "error")
@retry(stop=stop_after_delay(10), wait=wait_fixed(0.5), reraise=True)
def rpc_request(self, method, params=[], request_id=13, url=None): def rpc_request(self, method, params=[], request_id=13, url=None):
url = url if url else self.rpc_url url = url if url else self.rpc_url
data = {"jsonrpc": "2.0", "method": method, "id": request_id} data = {"jsonrpc": "2.0", "method": method, "id": request_id}
@ -50,7 +51,10 @@ class RpcClient:
logging.info(f"Sending POST request to url {url} with data: {json.dumps(data, sort_keys=True, indent=4)}") 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) response = self.client.post(url, json=data)
try: try:
logging.info(f"Got response: {json.dumps(response.json(), sort_keys=True, indent=4)}") resp_json = response.json()
logging.info(f"Got response: {json.dumps(resp_json, sort_keys=True, indent=4)}")
if resp_json.get("error"):
assert "JSON-RPC client is unavailable" != resp_json["error"]
except JSONDecodeError: except JSONDecodeError:
logging.info(f"Got response: {response.content}") logging.info(f"Got response: {response.content}")
return response return response

View File

@ -3,10 +3,26 @@ import logging
import time import time
import websocket import websocket
import os
from pathlib import Path
from constants import SIGNALS_DIR, LOG_SIGNALS_TO_FILE
from datetime import datetime
from enum import Enum
class SignalType(Enum):
MESSAGES_NEW = "messages.new"
MESSAGE_DELIVERED = "message.delivered"
NODE_READY = "node.ready"
NODE_STARTED = "node.started"
NODE_LOGIN = "node.login"
MEDIASERVER_STARTED = "mediaserver.started"
WALLET_SUGGESTED_ROUTES = "wallet.suggested.routes"
WALLET_ROUTER_SIGN_TRANSACTIONS = "wallet.router.sign-transactions"
WALLET_ROUTER_SENDING_TRANSACTIONS_STARTED = "wallet.router.sending-transactions-started"
WALLET_TRANSACTION_STATUS_CHANGED = "wallet.transaction.status-changed"
WALLET_ROUTER_TRANSACTIONS_SENT = "wallet.router.transactions-sent"
class SignalClient: class SignalClient:
def __init__(self, ws_url, await_signals): def __init__(self, ws_url, await_signals):
self.url = f"{ws_url}/signals" self.url = f"{ws_url}/signals"
@ -24,14 +40,20 @@ class SignalClient:
"accept_fn": None "accept_fn": None
} for signal in self.await_signals } for signal in self.await_signals
} }
if LOG_SIGNALS_TO_FILE:
self.signal_file_path = os.path.join(SIGNALS_DIR, f"signal_{ws_url.split(':')[-1]}_{datetime.now().strftime('%H%M%S')}.log")
Path(SIGNALS_DIR).mkdir(parents=True, exist_ok=True)
def on_message(self, ws, signal): def on_message(self, ws, signal):
signal = json.loads(signal) signal_data = json.loads(signal)
signal_type = signal.get("type") if LOG_SIGNALS_TO_FILE:
self.write_signal_to_file(signal_data)
signal_type = signal_data.get("type")
if signal_type in self.await_signals: if signal_type in self.await_signals:
accept_fn = self.received_signals[signal_type]["accept_fn"] accept_fn = self.received_signals[signal_type]["accept_fn"]
if not accept_fn or accept_fn(signal): if not accept_fn or accept_fn(signal_data):
self.received_signals[signal_type]["received"].append(signal) self.received_signals[signal_type]["received"].append(signal_data)
def check_signal_type(self, signal_type): def check_signal_type(self, signal_type):
if signal_type not in self.await_signals: if signal_type not in self.await_signals:
@ -65,6 +87,24 @@ class SignalClient:
return self.received_signals[signal_type]["received"][-1] return self.received_signals[signal_type]["received"][-1]
return self.received_signals[signal_type]["received"][-delta_count:] return self.received_signals[signal_type]["received"][-delta_count:]
def find_signal_containing_pattern(self, signal_type, event_pattern, timeout=20):
start_time = time.time()
while True:
if time.time() - start_time >= timeout:
raise TimeoutError(
f"Signal {signal_type} containing {event_pattern} is not received in {timeout} seconds"
)
if not self.received_signals.get(signal_type):
time.sleep(0.2)
continue
for event in self.received_signals[signal_type]["received"]:
if event_pattern in str(event):
logging.info(
f"Signal {signal_type} containing {event_pattern} is received in {round(time.time() - start_time)} seconds"
)
return event
time.sleep(0.2)
def _on_error(self, ws, error): def _on_error(self, ws, error):
logging.error(f"Error: {error}") logging.error(f"Error: {error}")
@ -81,3 +121,8 @@ class SignalClient:
on_close=self._on_close) on_close=self._on_close)
ws.on_open = self._on_open ws.on_open = self._on_open
ws.run_forever() ws.run_forever()
def write_signal_to_file(self, signal_data):
with open(self.signal_file_path, "a+") as file:
json.dump(signal_data, file)
file.write("\n")

View File

@ -4,12 +4,13 @@ import time
import random import random
import threading import threading
import requests import requests
from tenacity import retry, stop_after_delay, wait_fixed
from clients.signals import SignalClient from clients.signals import SignalClient
from clients.rpc import RpcClient from clients.rpc import RpcClient
from datetime import datetime from datetime import datetime
from conftest import option from conftest import option
from constants import user_1 from constants import user_1, DEFAULT_DISPLAY_NAME
@ -66,7 +67,7 @@ class StatusBackend(RpcClient, SignalClient):
} }
return self.api_valid_request(method, data) return self.api_valid_request(method, data)
def create_account_and_login(self, display_name="Mr_Meeseeks", password=user_1.password): def create_account_and_login(self, display_name=DEFAULT_DISPLAY_NAME, password=user_1.password):
data_dir = f"dataDir_{datetime.now().strftime('%Y%m%d_%H%M%S')}" data_dir = f"dataDir_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
method = "CreateAccountAndLogin" method = "CreateAccountAndLogin"
data = { data = {
@ -78,7 +79,7 @@ class StatusBackend(RpcClient, SignalClient):
} }
return self.api_valid_request(method, data) return self.api_valid_request(method, data)
def restore_account_and_login(self, display_name="Mr_Meeseeks", user=user_1): def restore_account_and_login(self, display_name=DEFAULT_DISPLAY_NAME, user=user_1):
method = "RestoreAccountAndLogin" method = "RestoreAccountAndLogin"
data = { data = {
"rootDataDir": "/", "rootDataDir": "/",
@ -118,6 +119,7 @@ class StatusBackend(RpcClient, SignalClient):
time.sleep(3) time.sleep(3)
raise TimeoutError(f"RPC client was not started after {timeout} seconds") 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=[]): def start_messenger(self, params=[]):
method = "wakuext_startMessenger" method = "wakuext_startMessenger"
response = self.rpc_request(method, params) response = self.rpc_request(method, params)
@ -139,3 +141,29 @@ class StatusBackend(RpcClient, SignalClient):
method = "settings_getSettings" method = "settings_getSettings"
response = self.rpc_request(method, params) response = self.rpc_request(method, params)
self.verify_is_valid_json_rpc_response(response) 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, params=[]):
method = "wakuext_sendContactRequest"
response = self.rpc_request(method, params)
self.verify_is_valid_json_rpc_response(response)
return response.json()
def send_message(self, params=[]):
method = "wakuext_sendOneToOneMessage"
response = self.rpc_request(method, params)
self.verify_is_valid_json_rpc_response(response)
return response.json()

View File

@ -1,4 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
import os
@dataclass @dataclass
@ -21,3 +22,8 @@ user_2 = Account(
password="Strong12345", password="Strong12345",
passphrase="test test test test test test test test test test nest junk" passphrase="test test test test test test test test test test nest junk"
) )
DEFAULT_DISPLAY_NAME = "Mr_Meeseeks"
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))
TESTS_DIR = os.path.join(PROJECT_ROOT, "tests-functional")
SIGNALS_DIR = os.path.join(TESTS_DIR, "signals")
LOG_SIGNALS_TO_FILE = False # used for debugging purposes

View File

@ -4,3 +4,5 @@ pytest==6.2.4
requests==2.31.0 requests==2.31.0
genson~=1.2.2 genson~=1.2.2
websocket-client~=1.4.2 websocket-client~=1.4.2
tenacity~=9.0.0
pytest-dependency~=0.6.0

View File

@ -1,3 +1,4 @@
from contextlib import contextmanager
import json import json
import logging import logging
import threading import threading
@ -6,10 +7,10 @@ from collections import namedtuple
import pytest import pytest
from clients.signals import SignalClient from clients.signals import SignalClient, SignalType
from clients.status_backend import RpcClient, StatusBackend from clients.status_backend import RpcClient, StatusBackend
from conftest import option from conftest import option
from constants import user_1, user_2 from constants import user_1, user_2, DEFAULT_DISPLAY_NAME
class StatusDTestCase: class StatusDTestCase:
@ -24,7 +25,7 @@ class StatusDTestCase:
class StatusBackendTestCase: class StatusBackendTestCase:
await_signals = [ await_signals = [
"node.ready" SignalType.NODE_READY.value
] ]
def setup_class(self): def setup_class(self):
@ -32,7 +33,7 @@ class StatusBackendTestCase:
self.rpc_client.init_status_backend() self.rpc_client.init_status_backend()
self.rpc_client.restore_account_and_login() self.rpc_client.restore_account_and_login()
self.rpc_client.wait_for_signal("node.ready") self.rpc_client.wait_for_signal(SignalType.NODE_READY.value)
self.network_id = 31337 self.network_id = 31337
@ -150,3 +151,63 @@ 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 NetworkConditionTestCase:
@contextmanager
def add_latency(self):
pass
#TODO: To be implemented when we have docker exec capability
@contextmanager
def add_packet_loss(self):
pass
#TODO: To be implemented when we have docker exec capability
@contextmanager
def add_low_bandwith(self):
pass
#TODO: To be implemented when we have docker exec capability
@contextmanager
def node_pause(self, node):
pass
#TODO: To be implemented when we have docker exec capability
class OneToOneMessageTestCase(NetworkConditionTestCase):
def initialize_backend(self, await_signals, display_name=DEFAULT_DISPLAY_NAME, url=None):
backend = StatusBackend(await_signals=await_signals, url=url)
backend.init_status_backend()
backend.create_account_and_login(display_name=display_name)
backend.start_messenger()
return backend
def validate_event_against_response(self, event, fields_to_validate, response):
messages_in_event = event["event"]["messages"]
assert len(messages_in_event) > 0, "No messages found in the event"
response_chat = response["result"]["chats"][0]
message_id = response_chat["lastMessage"]["id"]
message = next((message for message in messages_in_event if message["id"] == message_id), None)
assert message, f"Message with ID {message_id} not found in the event"
message_mismatch = []
for response_field, event_field in fields_to_validate.items():
response_value = response_chat["lastMessage"][response_field]
event_value = message[event_field]
if response_value != event_value:
message_mismatch.append(
f"Field '{response_field}': Expected '{response_value}', Found '{event_value}'"
)
if not message_mismatch:
return
raise AssertionError(
"Some Sender RPC responses are not matching the signals received by the receiver.\n"
"Details of mismatches:\n" +
"\n".join(message_mismatch)
)

View File

@ -1,8 +1,8 @@
from test_cases import StatusBackend from test_cases import StatusBackend
import pytest import pytest
from clients.signals import SignalType
import os import os
@pytest.mark.create_account @pytest.mark.create_account
@pytest.mark.rpc @pytest.mark.rpc
class TestInitialiseApp: class TestInitialiseApp:
@ -12,10 +12,10 @@ class TestInitialiseApp:
await_signals = [ await_signals = [
"mediaserver.started", SignalType.MEDIASERVER_STARTED.value,
"node.started", SignalType.NODE_STARTED.value,
"node.ready", SignalType.NODE_READY.value,
"node.login", SignalType.NODE_LOGIN.value,
] ]
backend_client = StatusBackend(await_signals) backend_client = StatusBackend(await_signals)
@ -24,13 +24,13 @@ class TestInitialiseApp:
assert backend_client is not None assert backend_client is not None
backend_client.verify_json_schema( backend_client.verify_json_schema(
backend_client.wait_for_signal("mediaserver.started"), "signal_mediaserver_started") backend_client.wait_for_signal(SignalType.MEDIASERVER_STARTED.value), "signal_mediaserver_started")
backend_client.verify_json_schema( backend_client.verify_json_schema(
backend_client.wait_for_signal("node.started"), "signal_node_started") backend_client.wait_for_signal(SignalType.NODE_STARTED.value), "signal_node_started")
backend_client.verify_json_schema( backend_client.verify_json_schema(
backend_client.wait_for_signal("node.ready"), "signal_node_ready") backend_client.wait_for_signal(SignalType.NODE_READY.value), "signal_node_ready")
backend_client.verify_json_schema( backend_client.verify_json_schema(
backend_client.wait_for_signal("node.login"), "signal_node_login") backend_client.wait_for_signal(SignalType.NODE_LOGIN.value), "signal_node_login")
@pytest.mark.rpc @pytest.mark.rpc

View File

@ -0,0 +1,75 @@
from time import sleep
from uuid import uuid4
import pytest
from test_cases import OneToOneMessageTestCase
from constants import DEFAULT_DISPLAY_NAME
from clients.signals import SignalType
@pytest.mark.rpc
class TestOneToOneMessages(OneToOneMessageTestCase):
@pytest.fixture(scope="class", autouse=True)
def setup_nodes(self, request):
await_signals = [
SignalType.MESSAGES_NEW.value,
SignalType.MESSAGE_DELIVERED.value,
]
request.cls.sender = self.sender = self.initialize_backend(await_signals=await_signals)
request.cls.receiver = self.receiver = self.initialize_backend(await_signals=await_signals)
@pytest.mark.dependency(name="test_one_to_one_message_baseline")
def test_one_to_one_message_baseline(self, message_count=1):
pk_receiver = self.receiver.get_pubkey(DEFAULT_DISPLAY_NAME)
self.sender.send_contact_request([{"id": pk_receiver, "message": "contact_request"}])
sent_messages = []
for i in range(message_count):
message_text = f"test_message_{i+1}_{uuid4()}"
response = self.sender.send_message([{"id": pk_receiver, "message": message_text}])
sent_messages.append((message_text, response))
sleep(0.01)
for i, (message_text, response) in enumerate(sent_messages):
messages_new_event = self.receiver.find_signal_containing_pattern(SignalType.MESSAGES_NEW.value, event_pattern=message_text, timeout=60)
self.validate_event_against_response(
messages_new_event,
fields_to_validate={"text": "text"},
response=response,
)
@pytest.mark.dependency(depends=["test_one_to_one_message_baseline"])
def test_multiple_one_to_one_messages(self):
self.test_one_to_one_message_baseline(message_count=50)
@pytest.mark.dependency(depends=["test_one_to_one_message_baseline"])
@pytest.mark.skip(reason="Skipping until add_latency is implemented")
def test_one_to_one_message_with_latency(self):
with self.add_latency():
self.test_one_to_one_message_baseline()
@pytest.mark.dependency(depends=["test_one_to_one_message_baseline"])
@pytest.mark.skip(reason="Skipping until add_packet_loss is implemented")
def test_one_to_one_message_with_packet_loss(self):
with self.add_packet_loss():
self.test_one_to_one_message_baseline()
@pytest.mark.dependency(depends=["test_one_to_one_message_baseline"])
@pytest.mark.skip(reason="Skipping until add_low_bandwith is implemented")
def test_one_to_one_message_with_low_bandwidth(self):
with self.add_low_bandwith():
self.test_one_to_one_message_baseline()
@pytest.mark.dependency(depends=["test_one_to_one_message_baseline"])
@pytest.mark.skip(reason="Skipping until node_pause is implemented")
def test_one_to_one_message_with_node_pause_30_seconds(self):
pk_receiver = self.receiver.get_pubkey("Receiver")
self.sender.send_contact_request([{"id": pk_receiver, "message": "contact_request"}])
with self.node_pause(self.receiver):
message_text = f"test_message_{uuid4()}"
self.sender.send_message([{"id": pk_receiver, "message": message_text}])
sleep(30)
self.receiver.find_signal_containing_pattern(SignalType.MESSAGES_NEW.value, event_pattern=message_text)
self.sender.wait_for_signal("messages.delivered")

View File

@ -5,18 +5,18 @@ import pytest
from conftest import option from conftest import option
from constants import user_1, user_2 from constants import user_1, user_2
from test_cases import SignalTestCase from test_cases import SignalTestCase
from clients.signals import SignalType
@pytest.mark.rpc @pytest.mark.rpc
@pytest.mark.transaction @pytest.mark.transaction
@pytest.mark.wallet @pytest.mark.wallet
class TestTransactionFromRoute(SignalTestCase): class TestTransactionFromRoute(SignalTestCase):
await_signals = [ await_signals = [
"wallet.suggested.routes", SignalType.WALLET_SUGGESTED_ROUTES.value,
"wallet.router.sign-transactions", SignalType.WALLET_ROUTER_SIGN_TRANSACTIONS.value,
"wallet.router.sending-transactions-started", SignalType.WALLET_ROUTER_SENDING_TRANSACTIONS_STARTED.value,
"wallet.transaction.status-changed", SignalType.WALLET_TRANSACTION_STATUS_CHANGED.value,
"wallet.router.transactions-sent" SignalType.WALLET_ROUTER_TRANSACTIONS_SENT.value,
] ]
def test_tx_from_route(self): def test_tx_from_route(self):
@ -44,7 +44,7 @@ class TestTransactionFromRoute(SignalTestCase):
] ]
response = self.rpc_client.rpc_valid_request(method, params) response = self.rpc_client.rpc_valid_request(method, params)
routes = self.signal_client.wait_for_signal("wallet.suggested.routes") routes = self.signal_client.wait_for_signal(SignalType.WALLET_SUGGESTED_ROUTES.value)
assert routes['event']['Uuid'] == _uuid assert routes['event']['Uuid'] == _uuid
method = "wallet_buildTransactionsFromRoute" method = "wallet_buildTransactionsFromRoute"
@ -57,7 +57,7 @@ class TestTransactionFromRoute(SignalTestCase):
response = self.rpc_client.rpc_valid_request(method, params) response = self.rpc_client.rpc_valid_request(method, params)
wallet_router_sign_transactions = self.signal_client.wait_for_signal( wallet_router_sign_transactions = self.signal_client.wait_for_signal(
"wallet.router.sign-transactions") SignalType.WALLET_ROUTER_SIGN_TRANSACTIONS.value)
assert wallet_router_sign_transactions['event']['signingDetails']['signOnKeycard'] == False assert wallet_router_sign_transactions['event']['signingDetails']['signOnKeycard'] == False
transaction_hashes = wallet_router_sign_transactions['event']['signingDetails']['hashes'] transaction_hashes = wallet_router_sign_transactions['event']['signingDetails']['hashes']
@ -98,7 +98,7 @@ class TestTransactionFromRoute(SignalTestCase):
response = self.rpc_client.rpc_valid_request(method, params) response = self.rpc_client.rpc_valid_request(method, params)
tx_status = self.signal_client.wait_for_signal( tx_status = self.signal_client.wait_for_signal(
"wallet.transaction.status-changed") SignalType.WALLET_TRANSACTION_STATUS_CHANGED.value)
assert tx_status["event"]["chainId"] == 31337 assert tx_status["event"]["chainId"] == 31337
assert tx_status["event"]["status"] == "Success" assert tx_status["event"]["status"] == "Success"