import os from contextlib import ExitStack from unittest.mock import MagicMock, patch import LXMF import pytest from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st from meshchatx.meshchat import ReticulumMeshChat @pytest.fixture def mock_app(): with ExitStack() as stack: stack.enter_context(patch("meshchatx.meshchat.Database")) stack.enter_context(patch("meshchatx.meshchat.ConfigManager")) stack.enter_context(patch("meshchatx.meshchat.MessageHandler")) stack.enter_context(patch("meshchatx.meshchat.AnnounceManager")) stack.enter_context(patch("meshchatx.meshchat.ArchiverManager")) stack.enter_context(patch("meshchatx.meshchat.MapManager")) stack.enter_context(patch("meshchatx.meshchat.TelephoneManager")) stack.enter_context(patch("meshchatx.meshchat.VoicemailManager")) stack.enter_context(patch("meshchatx.meshchat.RingtoneManager")) stack.enter_context(patch("meshchatx.meshchat.RNCPHandler")) stack.enter_context(patch("meshchatx.meshchat.RNStatusHandler")) stack.enter_context(patch("meshchatx.meshchat.RNProbeHandler")) stack.enter_context(patch("meshchatx.meshchat.TranslatorHandler")) mock_async_utils = stack.enter_context(patch("meshchatx.meshchat.AsyncUtils")) stack.enter_context(patch("LXMF.LXMRouter")) mock_identity_class = stack.enter_context(patch("RNS.Identity")) stack.enter_context(patch("RNS.Reticulum")) stack.enter_context(patch("RNS.Transport")) stack.enter_context(patch("threading.Thread")) stack.enter_context( patch.object(ReticulumMeshChat, "announce_loop", return_value=None), ) stack.enter_context( patch.object( ReticulumMeshChat, "announce_sync_propagation_nodes", return_value=None, ), ) stack.enter_context( patch.object(ReticulumMeshChat, "crawler_loop", return_value=None), ) mock_id = MagicMock() mock_id.hash = b"test_hash_32_bytes_long_01234567" mock_id.get_private_key.return_value = b"test_private_key" mock_identity_class.return_value = mock_id # Make run_async a no-op that doesn't trigger coroutine warnings mock_async_utils.run_async = MagicMock(side_effect=lambda coroutine: None) app = ReticulumMeshChat( identity=mock_id, storage_dir="/tmp/meshchat_test", reticulum_config_dir="/tmp/meshchat_test", ) # Setup config mock to return real values to avoid background thread issues app.config = MagicMock() app.config.auto_announce_enabled.get.return_value = False app.config.auto_announce_interval_seconds.get.return_value = 600 app.config.last_announced_at.get.return_value = 0 app.config.lxmf_auto_sync_propagation_nodes_enabled.get.return_value = False app.config.lxmf_auto_sync_propagation_nodes_interval_seconds.get.return_value = 3600 app.config.lxmf_auto_sync_propagation_nodes_last_synced_at.get.return_value = 0 app.config.voicemail_enabled.get.return_value = True app.config.voicemail_auto_answer_delay_seconds.get.return_value = 0 app.config.voicemail_greeting.get.return_value = "Hello" app.config.voicemail_max_recording_seconds.get.return_value = 10 # Other required mocks for on_lxmf_delivery app.is_destination_blocked = MagicMock(return_value=False) app.check_spam_keywords = MagicMock(return_value=False) app.db_upsert_lxmf_message = MagicMock() app.handle_forwarding = MagicMock() app.update_lxmf_user_icon = MagicMock() app.websocket_broadcast = MagicMock() yield app @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given( field_data=st.one_of( st.lists( st.one_of( st.text(), st.binary(), st.integers(), st.floats(), st.booleans(), st.none(), ), min_size=0, max_size=10, ), st.dictionaries( keys=st.text(), values=st.one_of( st.text(), st.binary(), st.integers(), st.floats(), st.booleans(), st.none(), ), ), st.binary(), st.text(), ), ) def test_lxmf_icon_appearance_fuzzing(mock_app, field_data): """Fuzz LXMF.FIELD_ICON_APPEARANCE parsing in on_lxmf_delivery.""" mock_message = MagicMock() mock_message.get_fields.return_value = {LXMF.FIELD_ICON_APPEARANCE: field_data} mock_message.source_hash = os.urandom(16) mock_message.hash = os.urandom(16) try: mock_app.on_lxmf_delivery(mock_message) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given( attachments_data=st.lists( st.one_of( st.lists( st.one_of( st.text(), st.binary(), st.integers(), st.floats(), st.booleans(), st.none(), ), min_size=0, max_size=5, ), st.text(), st.binary(), st.none(), ), min_size=0, max_size=10, ), ) def test_lxmf_attachments_fuzzing(mock_app, attachments_data): """Fuzz LXMF.FIELD_FILE_ATTACHMENTS parsing.""" mock_message = MagicMock() mock_message.get_fields.return_value = { LXMF.FIELD_FILE_ATTACHMENTS: attachments_data, } mock_message.source_hash = os.urandom(16) mock_message.hash = os.urandom(16) try: mock_app.on_lxmf_delivery(mock_message) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given( image_data=st.one_of( st.lists( st.one_of( st.text(), st.binary(), st.integers(), st.floats(), st.booleans(), st.none(), ), min_size=0, max_size=5, ), st.binary(), st.none(), ), ) def test_lxmf_image_field_fuzzing(mock_app, image_data): """Fuzz LXMF.FIELD_IMAGE parsing.""" mock_message = MagicMock() mock_message.get_fields.return_value = {LXMF.FIELD_IMAGE: image_data} mock_message.source_hash = os.urandom(16) mock_message.hash = os.urandom(16) try: mock_app.on_lxmf_delivery(mock_message) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given( audio_data=st.one_of( st.lists( st.one_of( st.text(), st.binary(), st.integers(), st.floats(), st.booleans(), st.none(), ), min_size=0, max_size=5, ), st.binary(), st.none(), ), ) def test_lxmf_audio_field_fuzzing(mock_app, audio_data): """Fuzz LXMF.FIELD_AUDIO parsing.""" mock_message = MagicMock() mock_message.get_fields.return_value = {LXMF.FIELD_AUDIO: audio_data} mock_message.source_hash = os.urandom(16) mock_message.hash = os.urandom(16) try: mock_app.on_lxmf_delivery(mock_message) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given( filename=st.text(min_size=0, max_size=1000), file_bytes=st.binary(min_size=0, max_size=10000), ) def test_attachment_filename_security(mock_app, filename, file_bytes): """Test for potential directory traversal or malicious filenames in attachments.""" mock_message = MagicMock() mock_message.get_fields.return_value = { LXMF.FIELD_FILE_ATTACHMENTS: [[filename, file_bytes]], } mock_message.source_hash = os.urandom(16) mock_message.hash = os.urandom(16) try: mock_app.on_lxmf_delivery(mock_message) mock_app.convert_lxmf_message_to_dict(mock_message) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given(caller_id_bytes=st.binary(min_size=0, max_size=1000)) def test_telephone_callback_fuzzing(mock_app, caller_id_bytes): """Fuzz telephone manager callbacks with malformed identity bytes.""" try: mock_identity = MagicMock() mock_identity.hash = caller_id_bytes mock_app.telephone_manager.on_telephone_ringing(mock_identity) mock_app.telephone_manager.on_telephone_call_established(mock_identity) mock_app.telephone_manager.on_telephone_call_ended(mock_identity) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given( data=st.dictionaries( keys=st.text(), values=st.one_of( st.text(), st.binary(), st.integers(), st.floats(), st.lists(st.text()), st.dictionaries(keys=st.text(), values=st.text()), ), ), ) def test_message_dao_upsert_fuzzing(mock_app, data): """Fuzz MessageDAO.upsert_lxmf_message with varied dictionary data.""" try: mock_app.database.messages.upsert_lxmf_message(data) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given( title_bytes=st.binary(min_size=0, max_size=1000), content_bytes=st.binary(min_size=0, max_size=5000), ) def test_lxmf_message_decoding_fuzzing(mock_app, title_bytes, content_bytes): """Fuzz LXMF message title and content decoding.""" mock_message = MagicMock() mock_message.title = title_bytes mock_message.content = content_bytes mock_message.hash = os.urandom(16) mock_message.source_hash = os.urandom(16) mock_message.destination_hash = os.urandom(16) mock_message.incoming = True mock_message.state = LXMF.LXMessage.DELIVERED mock_message.method = LXMF.LXMessage.DIRECT mock_message.progress = 1.0 mock_message.timestamp = 123456789.0 mock_message.rssi = -50 mock_message.snr = 10 mock_message.q = 100 mock_message.get_fields.return_value = {} try: mock_app.convert_lxmf_message_to_dict(mock_message) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given(greeting_text=st.text(min_size=0, max_size=1000)) def test_voicemail_greeting_fuzzing(mock_app, greeting_text): """Fuzz voicemail greeting generation with varied text.""" mock_app.voicemail_manager.has_espeak = True mock_app.voicemail_manager.has_ffmpeg = True mock_app.voicemail_manager.espeak_path = "/usr/bin/espeak" mock_app.voicemail_manager.ffmpeg_path = "/usr/bin/ffmpeg" with patch("subprocess.run") as mock_run: try: mock_app.voicemail_manager.generate_greeting(greeting_text) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given(caller_hash=st.binary(min_size=0, max_size=32)) def test_voicemail_incoming_call_fuzzing(mock_app, caller_hash): """Fuzz voicemail incoming call handling.""" mock_identity = MagicMock() mock_identity.hash = caller_hash try: mock_app.voicemail_manager.handle_incoming_call(mock_identity) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given( source_hash=st.text(min_size=0, max_size=64), recipient_hash=st.text(min_size=0, max_size=64), dest_hash=st.text(min_size=0, max_size=64), ) def test_forwarding_manager_mapping_fuzzing( mock_app, source_hash, recipient_hash, dest_hash, ): """Fuzz forwarding manager mapping creation.""" try: mock_app.forwarding_manager.get_or_create_mapping( source_hash, recipient_hash, dest_hash, ) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given(uri=st.text(min_size=0, max_size=5000)) def test_lxm_ingest_uri_fuzzing(mock_app, uri): """Fuzz the lxm.ingest_uri WebSocket handler.""" mock_client = MagicMock() mock_client.send_str = MagicMock() try: # We need to wrap it in a task since it's async import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete( mock_app.on_websocket_data_received( mock_client, {"type": "lxm.ingest_uri", "uri": uri}, ), ) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given( config_data=st.dictionaries( keys=st.text(), values=st.one_of( st.text(), st.integers(), st.booleans(), st.none(), st.lists(st.text()), st.dictionaries(keys=st.text(), values=st.text()), ), ), ) def test_update_config_fuzzing(mock_app, config_data): """Fuzz the update_config method with randomized dictionary data.""" try: import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(mock_app.update_config(config_data)) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given(large_string=st.text(min_size=1000, max_size=10000)) def test_large_payload_dos_resistance(mock_app, large_string): """Check resistance to DoS via large strings in various fields.""" mock_message = MagicMock() mock_message.title = large_string.encode() mock_message.content = large_string.encode() mock_message.hash = os.urandom(16) mock_message.source_hash = os.urandom(16) mock_message.get_fields.return_value = {} try: mock_app.on_lxmf_delivery(mock_message) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given( nested_data=st.recursive( st.one_of(st.text(), st.integers()), lambda children: st.dictionaries(st.text(), children) | st.lists(children), max_leaves=100, ), ) def test_websocket_recursion_fuzzing(mock_app, nested_data): """Fuzz the WebSocket handler with deeply nested JSON data.""" mock_client = MagicMock() try: import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete( mock_app.on_websocket_data_received( mock_client, {"type": "ping", "data": nested_data}, ), ) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given(dest_hash=st.text(), content=st.text()) def test_lxm_generate_paper_uri_fuzzing(mock_app, dest_hash, content): """Fuzz paper URI generation with randomized inputs.""" mock_client = MagicMock() try: import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete( mock_app.on_websocket_data_received( mock_client, { "type": "lxm.generate_paper_uri", "destination_hash": dest_hash, "content": content, }, ), ) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given( lon=st.floats(allow_nan=False, allow_infinity=False), lat=st.floats(allow_nan=False, allow_infinity=False), zoom=st.integers(min_value=-100, max_value=100), ) def test_map_manager_coord_fuzzing(mock_app, lon, lat, zoom): """Fuzz coordinate to tile conversion in MapManager.""" try: mock_app.map_manager._lonlat_to_tile(lon, lat, zoom) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given( text=st.text(), source_lang=st.text(min_size=0, max_size=10), target_lang=st.text(min_size=0, max_size=10), ) def test_translator_handler_fuzzing(mock_app, text, source_lang, target_lang): """Fuzz the TranslatorHandler translate_text method.""" try: # Mock dependencies mock_app.translator_handler.has_requests = False mock_app.translator_handler.has_argos = False mock_app.translator_handler.translate_text(text, source_lang, target_lang) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given(dest_hash=st.text(), icon_name=st.text(), fg_color=st.text(), bg_color=st.text()) def test_update_lxmf_user_icon_fuzzing( mock_app, dest_hash, icon_name, fg_color, bg_color, ): """Fuzz user icon update logic with malformed strings.""" try: mock_app.update_lxmf_user_icon(dest_hash, icon_name, fg_color, bg_color) except Exception: pass @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None) @given(binary_data=st.binary(min_size=0, max_size=10000)) def test_rns_identity_load_fuzzing(mock_app, binary_data): """Fuzz RNS.Identity loading with random binary data.""" try: import RNS try: RNS.Identity.from_bytes(binary_data) except Exception: pass try: id_inst = RNS.Identity(create_keys=False) id_inst.load_private_key(binary_data) except Exception: pass try: id_inst = RNS.Identity(create_keys=False) id_inst.load_public_key(binary_data) except Exception: pass except Exception: pass