Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test_: one to one messages #6119

Merged
merged 9 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions tests-functional/clients/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging
import jsonschema
import requests

from tenacity import retry, stop_after_delay, wait_fixed
from conftest import option
from json import JSONDecodeError

Expand Down Expand Up @@ -42,6 +42,7 @@ def verify_is_json_rpc_error(self, response):
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}
Expand All @@ -50,7 +51,10 @@ def rpc_request(self, method, params=[], request_id=13, url=None):
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
Expand Down
55 changes: 50 additions & 5 deletions tests-functional/clients/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,26 @@
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"
fbarbu15 marked this conversation as resolved.
Show resolved Hide resolved

class SignalClient:

def __init__(self, ws_url, await_signals):
self.url = f"{ws_url}/signals"

Expand All @@ -24,14 +40,20 @@ def __init__(self, ws_url, await_signals):
"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:
Expand Down Expand Up @@ -65,6 +87,24 @@ def wait_for_signal(self, signal_type, timeout=20):
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 not self.received_signals.get(signal_type):
if time.time() - start_time >= timeout:
raise TimeoutError(
f"Signal {signal_type} containing {event_pattern} is not received in {timeout} seconds"
)
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}")

Expand All @@ -81,3 +121,8 @@ def _connect(self):
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")
34 changes: 31 additions & 3 deletions tests-functional/clients/status_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
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



Expand Down Expand Up @@ -66,7 +67,7 @@ def init_status_backend(self, data_dir="/"):
}
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 = {
Expand All @@ -78,7 +79,7 @@ def create_account_and_login(self, display_name="Mr_Meeseeks", password=user_1.p
}
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": "/",
Expand Down Expand Up @@ -118,6 +119,7 @@ def restore_account_and_wait_for_rpc_client_to_start(self, timeout=60):
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)
Expand All @@ -139,3 +141,29 @@ 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, 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()
6 changes: 6 additions & 0 deletions tests-functional/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass
import os


@dataclass
Expand All @@ -21,3 +22,8 @@ class Account:
password="Strong12345",
passphrase="test test test test test test test test test test nest junk"
)
DEFAULT_DISPLAY_NAME = "Mr_Meeseeks"
fbarbu15 marked this conversation as resolved.
Show resolved Hide resolved
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
fbarbu15 marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions tests-functional/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
71 changes: 67 additions & 4 deletions tests-functional/tests/test_cases.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from contextlib import contextmanager
import json
import logging
import threading
Expand All @@ -6,10 +7,10 @@

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:
Expand All @@ -24,15 +25,15 @@ def setup_method(self):
class StatusBackendTestCase:

await_signals = [
"node.ready"
SignalType.NODE_READY.value
]

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.rpc_client.wait_for_signal(SignalType.NODE_READY.value)

self.network_id = 31337

Expand Down Expand Up @@ -150,3 +151,65 @@ def setup_method(self):
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]

mismatch_details = []
for message in messages_in_event:
match = True
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:
match = False
message_mismatch.append(
f"Field '{response_field}': Expected '{response_value}', Found '{event_value}'"
)
if match:
return
fbarbu15 marked this conversation as resolved.
Show resolved Hide resolved

mismatch_details.append(f"Message ID: {message['id']}, Mismatches: {message_mismatch}")

raise AssertionError(
"Some Sender RPC responses are not matching the signals received by the receiver.\n"
"Details of mismatches:\n" +
"\n".join(mismatch_details)
)

18 changes: 9 additions & 9 deletions tests-functional/tests/test_init_status_app.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -12,10 +12,10 @@ def test_init_app(self):

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)
Expand All @@ -24,13 +24,13 @@ def test_init_app(self):

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
Expand Down
Loading