diff --git a/tests-functional/constants.py b/tests-functional/constants.py index aaae0b54b7..386504e5c9 100644 --- a/tests-functional/constants.py +++ b/tests-functional/constants.py @@ -48,5 +48,6 @@ class Account: REMOVE_TC_CMD = "sudo tc qdisc del dev eth0 root" NUM_CONTACT_REQUESTS = int(os.getenv("NUM_CONTACT_REQUESTS", "5")) NUM_MESSAGES = int(os.getenv("NUM_MESSAGES", "20")) -DELAY_BETWEEN_MESSAGES = int(os.getenv("NUM_MESSAGES", "1")) +DELAY_BETWEEN_MESSAGES = int(os.getenv("DELAY_BETWEEN_MESSAGES", "1")) EVENT_SIGNAL_TIMEOUT_SEC = int(os.getenv("EVENT_SIGNAL_TIMEOUT_SEC", "4")) +PRIVATE_GROUPS = int(os.getenv("PRIVATE_GROUPS", "20")) diff --git a/tests-functional/src/node/status_node.py b/tests-functional/src/node/status_node.py index 872944f228..ebba187fae 100644 --- a/tests-functional/src/node/status_node.py +++ b/tests-functional/src/node/status_node.py @@ -150,6 +150,10 @@ def send_contact_request(self, pubkey, message): params = [{"id": pubkey, "message": message}] return self.api.send_rpc_request("wakuext_sendContactRequest", params) + def accept_contact_request(self, chatId): + params = [{"id": chatId}] + return self.api.send_rpc_request("wakuext_acceptContactRequest", params) + def send_message(self, pubkey, message): params = [{"id": pubkey, "message": message}] return self.api.send_rpc_request("wakuext_sendOneToOneMessage", params) @@ -163,3 +167,13 @@ def resume_process(self): if self.pid: logger.info(f"Resuming node with pid: {self.pid}") os.kill(self.pid, signal.SIGCONT) + + def create_group_chat_with_members(self, pubkey_list, group_chat_name): + if not isinstance(pubkey_list, list): + raise TypeError("pubkey_list needs to be list") + params = [None, group_chat_name, pubkey_list] + return self.api.send_rpc_request("wakuext_createGroupChatWithMembers", params) + + def send_group_chat_message(self, group_id, message): + params = [{"id": group_id, "message": message}] + return self.api.send_rpc_request("wakuext_sendGroupChatMessage", params) diff --git a/tests-functional/src/steps/common.py b/tests-functional/src/steps/common.py index 87958e4c61..d137202ae7 100644 --- a/tests-functional/src/steps/common.py +++ b/tests-functional/src/steps/common.py @@ -103,9 +103,41 @@ def send_with_timestamp(self, send_method, id, message): return timestamp, message_id, response + def send_with_timestamp_for_group(self, send_method, id, message): + timestamp = datetime.now().strftime("%H:%M:%S") + response = send_method(id, message) + message_id = None + response_chats = response.get("result", {}).get("chats", []) + + if response_chats: + message_id = response_chats[0].get("id") + + return timestamp, message_id, response + def accept_contact_request(self, sending_node=None, receiving_node_pk=None): if not sending_node: sending_node = self.second_node if not receiving_node_pk: receiving_node_pk = self.first_node_pubkey sending_node.send_contact_request(receiving_node_pk, "hi") + + def create_group_chat_with_timestamp(self, sender_node, member_list, private_group_name): + timestamp = datetime.now().strftime("%H:%M:%S") + response = sender_node.create_group_chat_with_members(member_list, private_group_name) + response_messages = response["result"]["messages"] + message_id = None + for m in response_messages: + if private_group_name in m["text"]: + message_id = m["id"] + break + return timestamp, message_id + + def join_private_group(self, sender_node=None, members_list=None): + if not sender_node: + sender_node = self.second_node + if not members_list: + members_list = [self.first_node_pubkey] + response = sender_node.create_group_chat_with_members(members_list, "new_group") + self.private_group_id = response["result"]["chats"][0]["id"] + return self.private_group_id, response + diff --git a/tests-functional/tests/test_create_private_groups.py b/tests-functional/tests/test_create_private_groups.py new file mode 100644 index 0000000000..1f27bc1b7b --- /dev/null +++ b/tests-functional/tests/test_create_private_groups.py @@ -0,0 +1,183 @@ +import pytest +from src.libs.common import delay +from src.libs.custom_logger import get_custom_logger +from src.steps.common import StepsCommon +from constants import * +from validators.contact_request_validator import ContactRequestValidator +from validators.group_chat_validator import GroupChatValidator + +logger = get_custom_logger(__name__) + + +@pytest.mark.usefixtures("start_2_nodes") +class TestCreatePrivateGroups(StepsCommon): + def test_create_group_chat_baseline(self): + num_private_groups = PRIVATE_GROUPS + private_groups = [] + contact_request_sent = False + + for i in range(num_private_groups): + if i % 2 == 0: + sender_node = self.first_node + receiver_node = self.second_node + receiver_pubkey = self.second_node_pubkey + else: + sender_node = self.second_node + receiver_node = self.first_node + receiver_pubkey = self.first_node_pubkey + + if not contact_request_sent: + display_name = f"{receiver_node.name}_user" + contact_request_message = f"contact_request_{i}" + timestamp, message_id, contact_request_message, response = self.send_and_wait_for_message( + (sender_node, receiver_node), display_name, i + ) + + if not response: + raise AssertionError(f"Contact request failed between {sender_node.name} and {receiver_node.name}") + + chat_id = response["result"]["chats"][0]["lastMessage"]["id"] + accept_response = receiver_node.accept_contact_request(chat_id) + + if not accept_response: + raise AssertionError( + f"Failed to accept contact request on {receiver_node.name} for chatId {chat_id}") + + contact_request_sent = True + delay(10) + + group_name = f"private_group_from_{sender_node.name}_{i}" + try: + logger.info(f"Creating group '{group_name}' from {sender_node.name}") + timestamp, message_id, response = self.create_and_validate_private_group( + sender_node, [receiver_pubkey], group_name, timeout=30 + ) + + if not response: + raise AssertionError(f"Failed to create private group '{group_name}' from {sender_node.name}") + else: + logger.info(f"Private group '{group_name}' created successfully with message ID: {message_id}") + private_groups.append((timestamp, group_name, message_id, sender_node.name)) + + except AssertionError as e: + logger.info(f"Group creation validation failed: {e}") + + delay(5) + + missing_private_groups = [ + (ts, name, mid, node) for ts, name, mid, node in private_groups if mid is None + ] + + if missing_private_groups: + formatted_missing_groups = [ + f"Timestamp: {ts}, GroupName: {msg}, ID: {mid}, Node: {node}" for ts, msg, mid, node in + missing_private_groups + ] + raise AssertionError( + f"{len(missing_private_groups)} private groups out of {num_private_groups} were not created: " + + "\n".join(formatted_missing_groups) + ) + self.first_node.stop() + self.second_node.stop() + + def send_and_wait_for_message(self, nodes, display_name, index, timeout=10): + sender_node, receiver_node = nodes + + receiver_pubkey = receiver_node.get_pubkey(display_name) + contact_request_message = f"contact_request_{index}" + + timestamp, message_id, response = self.send_with_timestamp( + sender_node.send_contact_request, receiver_pubkey, contact_request_message + ) + + validator = ContactRequestValidator(response) + validator.run_all_validations(receiver_pubkey, display_name, contact_request_message) + + try: + receiver_node.wait_for_signal("history.request.started", timeout) + + messages_new_events = receiver_node.wait_for_complete_signal("messages.new", timeout) + messages_new_event = None + + for event in messages_new_events: + if "chats" in event.get("event", {}): + messages_new_event = event + try: + validator.validate_event_against_response( + messages_new_event, + fields_to_validate={ + "text": "text", + "displayName": "displayName", + "id": "id" + } + ) + break + except AssertionError as validation_error: + logger.error(f"Validation failed for event: {messages_new_event}, Error: {validation_error}") + continue + + if messages_new_event is None: + raise ValueError("No 'messages.new' event with 'chats' data found within the timeout period.") + + receiver_node.wait_for_signal("history.request.completed", timeout) + + except (TimeoutError, ValueError) as e: + logger.error(f"Signal validation failed: {str(e)}") + return timestamp, message_id, contact_request_message, None + + return timestamp, message_id, contact_request_message, response + + def create_and_validate_private_group(self, node, members_pubkeys, group_name, timeout=10): + timestamp, message_id, response = self.send_with_timestamp_for_group( + node.create_group_chat_with_members, members_pubkeys, group_name + ) + + if not response or "result" not in response or "chats" not in response["result"]: + raise AssertionError("Invalid response structure. Expected 'result' with 'chats' list.") + + chat_data = response["result"]["chats"][0] + validator = GroupChatValidator(chat_data) + + try: + validator.validate_fields( + { + "name": group_name, + "active": True, + "chatType": 3, + "members": [ + {"id": pubkey} for pubkey in members_pubkeys + ] + } + ) + except AssertionError as validation_error: + raise AssertionError(f"Validation failed for group chat creation: {validation_error}") + + try: + node.wait_for_signal("history.request.completed", timeout) + except TimeoutError: + logger.error("Timeout waiting for group chat creation events.") + return timestamp, message_id, None + + return timestamp, message_id, response + + def test_create_group_chat_with_latency(self): + with self.add_latency(): + self.test_create_group_chat_baseline() + + def test_create_group_chat_with_packet_loss(self): + with self.add_packet_loss(): + self.test_create_group_chat_baseline() + + def test_create_group_chat_with_low_bandwidth(self): + with self.add_low_bandwidth(): + self.test_create_group_chat_baseline() + + def test_create_group_with_node_pause(self): + with self.node_pause(self.first_node): + delay(10) + try: + self.test_create_group_chat_baseline() + except Exception as e: + logger.info(f"Expected exception occurred while node was paused: {e}") + assert "Read timed out" in str(e) or "ConnectionError" in str( + e), "Unexpected error type when node is paused" diff --git a/tests-functional/tests/test_private_group_messages.py b/tests-functional/tests/test_private_group_messages.py new file mode 100644 index 0000000000..01809a4338 --- /dev/null +++ b/tests-functional/tests/test_private_group_messages.py @@ -0,0 +1,225 @@ +import pytest +from constants import * +from src.libs.common import delay +from src.libs.custom_logger import get_custom_logger +from src.steps.common import StepsCommon +from validators.contact_request_validator import ContactRequestValidator +from validators.group_chat_validator import GroupChatValidator + +logger = get_custom_logger(__name__) + + +@pytest.mark.usefixtures("start_2_nodes") +class TestPrivateGroupMessages(StepsCommon): + def test_group_chat_messages_baseline(self): + num_private_groups = PRIVATE_GROUPS + private_groups = [] + contact_request_sent = False + + for i in range(num_private_groups): + if i % 2 == 0: + sender_node = self.first_node + receiver_node = self.second_node + receiver_pubkey = self.second_node_pubkey + else: + sender_node = self.second_node + receiver_node = self.first_node + receiver_pubkey = self.first_node_pubkey + + if not contact_request_sent: + display_name = f"{receiver_node.name}_user" + contact_request_message = f"contact_request_{i}" + timestamp, message_id, contact_request_message, response = self.send_and_wait_for_message( + (sender_node, receiver_node), display_name, i + ) + + if not response: + raise AssertionError( + f"Contact request failed between {sender_node.name} and {receiver_node.name}") + + chat_id = response["result"]["chats"][0]["lastMessage"]["id"] + accept_response = receiver_node.accept_contact_request(chat_id) + + if not accept_response: + raise AssertionError( + f"Failed to accept contact request on {receiver_node.name} for chatId {chat_id}" + ) + + contact_request_sent = True + delay(12) + + group_name = f"private_group_from_{sender_node.name}_{i}" + try: + timestamp, message_id, response = self.create_and_validate_private_group( + sender_node, [receiver_pubkey], group_name + ) + + if not response: + raise AssertionError("Failed to create private group. No valid response received.") + else: + logger.info(f"Private group '{group_name}' created successfully with message ID: {message_id}") + private_groups.append((timestamp, group_name, message_id, sender_node.name)) + + except AssertionError as e: + logger.error(f"Group creation validation failed: {e}") + + messages = [] + for ts, group_name, msg_id, sender in private_groups: + group_id, _ = self.join_private_group(sender_node=sender_node, members_list=[receiver_pubkey]) + message = f"message_{msg_id}" + timestamp, message_id, response = self.send_and_validate_message(sender_node, group_id, message) + messages.append((timestamp, message, message_id, sender_node.name)) + + if not response: + raise AssertionError(f"Failed to send message '{message}' in group '{group_name}'") + + missing_messages = [ + (ts, message, msg_id, sender) for ts, message, msg_id, sender in messages if msg_id is None + ] + + if missing_messages: + formatted_missing_messages = [ + f"Timestamp: {ts}, Message: {msg}, ID: {msg_id}, Sender: {sender}" for ts, msg, msg_id, sender in + missing_messages + ] + raise AssertionError( + f"{len(missing_messages)} messages out of {num_private_groups} were not received correctly: " + + "\n".join(formatted_missing_messages) + ) + self.first_node.stop() + self.second_node.stop() + + def send_and_wait_for_message(self, nodes, display_name, index, timeout=10): + sender_node, receiver_node = nodes + + receiver_pubkey = receiver_node.get_pubkey(display_name) + contact_request_message = f"contact_request_{index}" + + timestamp, message_id, response = self.send_with_timestamp( + sender_node.send_contact_request, receiver_pubkey, contact_request_message + ) + + validator = ContactRequestValidator(response) + validator.run_all_validations(receiver_pubkey, display_name, contact_request_message) + + try: + receiver_node.wait_for_signal("history.request.started", timeout) + + messages_new_events = receiver_node.wait_for_complete_signal("messages.new", timeout) + messages_new_event = None + + for event in messages_new_events: + if "chats" in event.get("event", {}): + messages_new_event = event + try: + validator.validate_event_against_response( + messages_new_event, + fields_to_validate={ + "text": "text", + "displayName": "displayName", + "id": "id" + } + ) + break + except AssertionError as validation_error: + logger.error(f"Validation failed for event: {messages_new_event}, Error: {validation_error}") + continue + + if messages_new_event is None: + raise ValueError("No 'messages.new' event with 'chats' data found within the timeout period.") + + receiver_node.wait_for_signal("history.request.completed", timeout) + + except (TimeoutError, ValueError) as e: + logger.error(f"Signal validation failed: {str(e)}") + return timestamp, message_id, contact_request_message, None + + return timestamp, message_id, contact_request_message, response + + def create_and_validate_private_group(self, node, members_pubkeys, group_name, timeout=10): + timestamp, message_id, response = self.send_with_timestamp_for_group( + node.create_group_chat_with_members, members_pubkeys, group_name + ) + + if not response or "result" not in response or "chats" not in response["result"]: + raise AssertionError("Invalid response structure. Expected 'result' with 'chats' list.") + + chat_data = response["result"]["chats"][0] + validator = GroupChatValidator(chat_data) + + try: + validator.validate_fields( + { + "name": group_name, + "active": True, + "chatType": 3, + "members": [ + {"id": pubkey} for pubkey in members_pubkeys + ] + } + ) + except AssertionError as validation_error: + raise AssertionError(f"Validation failed for group chat creation: {validation_error}") + + try: + node.wait_for_signal("history.request.completed", timeout) + except TimeoutError: + logger.error("Timeout waiting for group chat creation events.") + return timestamp, message_id, None + + return timestamp, message_id, response + + def send_and_validate_message(self, sender_node, group_id, message, timeout=10): + timestamp, message_id, response = self.send_with_timestamp( + sender_node.send_group_chat_message, group_id, message + ) + + if not response or "result" not in response or "chats" not in response["result"]: + raise AssertionError("Invalid response structure. Expected 'result' with chat message details.") + + message_data = response["result"]["chats"][0].get("lastMessage", {}) + if not message_data: + raise AssertionError("No 'lastMessage' data found in response.") + + validator = GroupChatValidator(message_data) + try: + validator.validate_message_fields( + { + "id": message_id, + "text": message, + "chatId": group_id, + "timestamp": timestamp + } + ) + except AssertionError as validation_error: + raise AssertionError(f"Validation failed for message send: {validation_error}") + + try: + sender_node.wait_for_signal("message.delivered", timeout) + except TimeoutError: + logger.error("Timeout waiting for message delivery.") + return timestamp, message_id, None + + return timestamp, message_id, response + + def test_group_chat_messages_with_latency(self): + with self.add_latency(): + self.test_group_chat_messages_baseline() + + def test_group_chat_messages_with_packet_loss(self): + with self.add_packet_loss(): + self.test_group_chat_messages_baseline() + + def test_group_chat_messages_with_low_bandwidth(self): + with self.add_low_bandwidth(): + self.test_group_chat_messages_baseline() + + def test_group_chat_messages_with_node_pause_10_seconds(self): + with self.node_pause(self.first_node): + delay(10) + try: + self.test_group_chat_messages_baseline() + except Exception as e: + logger.info(f"Expected exception occurred while node was paused: {e}") + assert "Read timed out" in str(e) or "ConnectionError" in str(e), \ + "Unexpected error type when node is paused" diff --git a/tests-functional/validators/contact_request_validator.py b/tests-functional/validators/contact_request_validator.py index f98d56fc5c..5496bf11c6 100644 --- a/tests-functional/validators/contact_request_validator.py +++ b/tests-functional/validators/contact_request_validator.py @@ -32,8 +32,8 @@ def validate_chat_data(self, expected_chat_id, expected_display_name, expected_t ) actual_contact_request_state = last_message.get("contactRequestState") - assert actual_contact_request_state == 1, ( - f"Unexpected contact request state: Expected '1', found '{actual_contact_request_state}'" + assert actual_contact_request_state >= 1, ( + f"Unexpected contact request state: Expected '1' or higher, found '{actual_contact_request_state}'" ) assert "compressedKey" in last_message, "Missing 'compressedKey' in last message" diff --git a/tests-functional/validators/group_chat_validator.py b/tests-functional/validators/group_chat_validator.py new file mode 100644 index 0000000000..fb8aadf21e --- /dev/null +++ b/tests-functional/validators/group_chat_validator.py @@ -0,0 +1,37 @@ +class GroupChatValidator: + def __init__(self, response_data): + self.response_data = response_data + + def validate_fields(self, expected_fields): + for field, expected_value in expected_fields.items(): + actual_value = self.response_data.get(field) + if isinstance(expected_value, list): + if not all( + any(member.get("id") == ev["id"] for member in actual_value) + for ev in expected_value + ): + raise AssertionError(f"Validation failed: Mismatched members for field '{field}'") + elif actual_value != expected_value: + raise AssertionError( + f"Validation failed for field '{field}': expected '{expected_value}', got '{actual_value}'") + + def validate_message_fields(self, expected_fields): + for field, expected_value in expected_fields.items(): + if field == "timestamp": + continue + + actual_value = self.response_data.get(field) + + if isinstance(expected_value, dict) and isinstance(actual_value, dict): + for sub_field, sub_value in expected_value.items(): + if actual_value.get(sub_field) != sub_value: + raise AssertionError( + f"Validation failed for nested field '{field}.{sub_field}': expected '{sub_value}', got '{actual_value.get(sub_field)}'" + ) + elif isinstance(expected_value, list) and isinstance(actual_value, list): + if not all(item in actual_value for item in expected_value): + raise AssertionError(f"Validation failed: Mismatched list items for field '{field}'") + elif actual_value != expected_value: + raise AssertionError( + f"Validation failed for field '{field}': expected '{expected_value}', got '{actual_value}'" + )