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
tests-functional/coverage
tests-functional/reports
tests-functional/signals
tests-functional/*.log
pyrightconfig.json
.venv
# generated files
mock

View File

@ -2,7 +2,7 @@ import json
import logging
import jsonschema
import requests
from tenacity import retry, stop_after_delay, wait_fixed
from conftest import option
from json import JSONDecodeError
@ -42,6 +42,7 @@ class RpcClient:
assert response.content
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):
url = url if url else self.rpc_url
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)}")
response = self.client.post(url, json=data)
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:
logging.info(f"Got response: {response.content}")
return response

View File

@ -3,10 +3,26 @@ import logging
import time
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:
def __init__(self, ws_url, await_signals):
self.url = f"{ws_url}/signals"
@ -24,14 +40,20 @@ class SignalClient:
"accept_fn": None
} 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):
signal = json.loads(signal)
signal_type = signal.get("type")
signal_data = json.loads(signal)
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:
accept_fn = self.received_signals[signal_type]["accept_fn"]
if not accept_fn or accept_fn(signal):
self.received_signals[signal_type]["received"].append(signal)
if not accept_fn or accept_fn(signal_data):
self.received_signals[signal_type]["received"].append(signal_data)
def check_signal_type(self, signal_type):
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"][-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):
logging.error(f"Error: {error}")
@ -81,3 +121,8 @@ class SignalClient:
on_close=self._on_close)
ws.on_open = self._on_open
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 threading
import requests
from tenacity import retry, stop_after_delay, wait_fixed
from clients.signals import SignalClient
from clients.rpc import RpcClient
from datetime import datetime
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)
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')}"
method = "CreateAccountAndLogin"
data = {
@ -78,7 +79,7 @@ class StatusBackend(RpcClient, SignalClient):
}
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"
data = {
"rootDataDir": "/",
@ -118,6 +119,7 @@ class StatusBackend(RpcClient, SignalClient):
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)
@ -139,3 +141,29 @@ class StatusBackend(RpcClient, SignalClient):
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, 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
import os
@dataclass
@ -21,3 +22,8 @@ user_2 = Account(
password="Strong12345",
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
genson~=1.2.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 logging
import threading
@ -6,10 +7,10 @@ from collections import namedtuple
import pytest
from clients.signals import SignalClient
from clients.signals import SignalClient, SignalType
from clients.status_backend import RpcClient, StatusBackend
from conftest import option
from constants import user_1, user_2
from constants import user_1, user_2, DEFAULT_DISPLAY_NAME
class StatusDTestCase:
@ -24,7 +25,7 @@ class StatusDTestCase:
class StatusBackendTestCase:
await_signals = [
"node.ready"
SignalType.NODE_READY.value
]
def setup_class(self):
@ -32,7 +33,7 @@ class StatusBackendTestCase:
self.rpc_client.init_status_backend()
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
@ -150,3 +151,63 @@ class SignalTestCase(StatusDTestCase):
websocket_thread = threading.Thread(target=self.signal_client._connect)
websocket_thread.daemon = True
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
import pytest
from clients.signals import SignalType
import os
@pytest.mark.create_account
@pytest.mark.rpc
class TestInitialiseApp:
@ -12,10 +12,10 @@ class TestInitialiseApp:
await_signals = [
"mediaserver.started",
"node.started",
"node.ready",
"node.login",
SignalType.MEDIASERVER_STARTED.value,
SignalType.NODE_STARTED.value,
SignalType.NODE_READY.value,
SignalType.NODE_LOGIN.value,
]
backend_client = StatusBackend(await_signals)
@ -24,13 +24,13 @@ class TestInitialiseApp:
assert backend_client is not None
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.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.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.wait_for_signal("node.login"), "signal_node_login")
backend_client.wait_for_signal(SignalType.NODE_LOGIN.value), "signal_node_login")
@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 constants import user_1, user_2
from test_cases import SignalTestCase
from clients.signals import SignalType
@pytest.mark.rpc
@pytest.mark.transaction
@pytest.mark.wallet
class TestTransactionFromRoute(SignalTestCase):
await_signals = [
"wallet.suggested.routes",
"wallet.router.sign-transactions",
"wallet.router.sending-transactions-started",
"wallet.transaction.status-changed",
"wallet.router.transactions-sent"
SignalType.WALLET_SUGGESTED_ROUTES.value,
SignalType.WALLET_ROUTER_SIGN_TRANSACTIONS.value,
SignalType.WALLET_ROUTER_SENDING_TRANSACTIONS_STARTED.value,
SignalType.WALLET_TRANSACTION_STATUS_CHANGED.value,
SignalType.WALLET_ROUTER_TRANSACTIONS_SENT.value,
]
def test_tx_from_route(self):
@ -44,7 +44,7 @@ class TestTransactionFromRoute(SignalTestCase):
]
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
method = "wallet_buildTransactionsFromRoute"
@ -57,7 +57,7 @@ class TestTransactionFromRoute(SignalTestCase):
response = self.rpc_client.rpc_valid_request(method, params)
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
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)
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"]["status"] == "Success"