mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-07-29 01:59:26 +00:00
Refactor backend tests
- Removed redundant comments from various test files to enhance readability and focus on the test logic. - Added a few tests for prop sync and markdown security.
This commit is contained in:
@@ -43,9 +43,6 @@ def global_mocks():
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_sqlite_connections():
|
||||
yield
|
||||
# After each test, try to close any lingering sqlite connections if possible
|
||||
# This is a bit hard globally without tracking them, but we can at least
|
||||
# trigger GC which often helps with ResourceWarnings.
|
||||
import gc
|
||||
|
||||
gc.collect()
|
||||
|
||||
@@ -65,7 +65,6 @@ def test_database_snapshot_restoration(temp_dir):
|
||||
|
||||
|
||||
def test_database_auto_backup_logic(temp_dir):
|
||||
# This test verifies the loop logic if possible, or just the backup method
|
||||
db_path = os.path.join(temp_dir, "test.db")
|
||||
db = Database(db_path)
|
||||
db.initialize()
|
||||
|
||||
@@ -66,7 +66,6 @@ def test_parse_nomadnetwork_node_display_name_property_based(name):
|
||||
def test_parse_lxmf_display_name_invalid_base64(data):
|
||||
# Generate something that is NOT valid base64 (by adding invalid chars)
|
||||
invalid_b64 = base64.b64encode(data).decode("utf-8") + "!!!"
|
||||
# This should not crash, it should just return the default value or fail gracefully
|
||||
result = parse_lxmf_display_name(invalid_b64)
|
||||
assert isinstance(result, str)
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ def test_docs_manager_storage_dir_fallback(tmp_path):
|
||||
|
||||
|
||||
def test_docs_manager_readonly_public_dir_handling(tmp_path):
|
||||
# This test simulates a read-only public dir without storage_dir
|
||||
public_dir = tmp_path / "readonly_public"
|
||||
public_dir.mkdir()
|
||||
|
||||
@@ -189,7 +188,6 @@ def test_extract_docs_malformed_zip(docs_manager, temp_dirs):
|
||||
# 1. Zip with no folders at all
|
||||
create_mock_zip(zip_path, ["file_at_root.txt"])
|
||||
try:
|
||||
# This might fail with IndexError at namelist()[0].split('/')[0] if no slash
|
||||
docs_manager._extract_docs(zip_path)
|
||||
except (IndexError, Exception):
|
||||
pass # Expected or at least handled by not crashing the whole app
|
||||
|
||||
@@ -33,7 +33,6 @@ from meshchatx.src.backend.telemetry_utils import Telemeter
|
||||
def test_telemetry_unpack_fuzzing(data):
|
||||
"""Fuzz the telemetry unpacking logic with random binary data."""
|
||||
try:
|
||||
# This should not raise unhandled exceptions
|
||||
Telemeter.from_packed(data)
|
||||
except Exception:
|
||||
# We expect some failures for invalid packed data, but no crashes
|
||||
@@ -113,7 +112,6 @@ def test_display_name_parsing_fuzzing(app_data_base64):
|
||||
def test_lxmf_fields_parsing_fuzzing(fields_data):
|
||||
"""Fuzz the parsing of LXMF message fields."""
|
||||
try:
|
||||
# This simulates how meshchat.py processes fields in on_lxmf_delivery
|
||||
for field_id, field_data in fields_data.items():
|
||||
if field_id == 0x01: # FIELD_COMMANDS
|
||||
try:
|
||||
@@ -514,10 +512,7 @@ def test_malformed_announce_data(mock_app):
|
||||
def test_malformed_message_data(mock_app):
|
||||
"""Test handling of malformed LXMF messages."""
|
||||
mock_message = MagicMock()
|
||||
# Simulate missing attributes or methods
|
||||
del mock_message.source_hash
|
||||
|
||||
# This should be caught by the try-except in on_lxmf_delivery
|
||||
mock_app.on_lxmf_delivery(mock_message)
|
||||
|
||||
|
||||
|
||||
@@ -246,7 +246,6 @@ def test_lxmf_message_construction_fuzzing(mock_app, content, title, fields):
|
||||
)
|
||||
def test_database_record_fuzzing(mock_app, table_name, data):
|
||||
"""Fuzz database record insertion logic (simulated)."""
|
||||
# This tests the DAO layer's resilience to weird data types if they aren't properly sanitized
|
||||
try:
|
||||
dao = None
|
||||
if table_name == "messages" and hasattr(mock_app.database, "messages"):
|
||||
@@ -350,6 +349,36 @@ def test_markdown_renderer_fuzzing(text):
|
||||
pytest.fail(f"MarkdownRenderer crashed: {e}")
|
||||
|
||||
|
||||
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
||||
@given(
|
||||
text=st.one_of(
|
||||
st.text(min_size=0, max_size=5000),
|
||||
st.sampled_from([
|
||||
"<script>alert(1)</script>",
|
||||
"[x](javascript:alert(1))",
|
||||
"[x](data:text/html,<script>alert(1)</script>)",
|
||||
"**" * 2000,
|
||||
"#" * 2000,
|
||||
"`" * 2000,
|
||||
"[](" * 500 + ")" * 500,
|
||||
"\x00\x01\x02\n\t",
|
||||
"\ufffd" * 100,
|
||||
]),
|
||||
),
|
||||
)
|
||||
def test_markdown_renderer_dangerous_patterns(text):
|
||||
"""Fuzz markdown renderer with known risky patterns (XSS, ReDoS, control chars)."""
|
||||
from meshchatx.src.backend.markdown_renderer import MarkdownRenderer
|
||||
|
||||
try:
|
||||
html_out = MarkdownRenderer.render(text)
|
||||
assert isinstance(html_out, str)
|
||||
assert "<script>" not in html_out
|
||||
assert "javascript:" not in html_out
|
||||
except Exception as e:
|
||||
pytest.fail(f"MarkdownRenderer crashed on dangerous pattern: {e}")
|
||||
|
||||
|
||||
# LXMF Message Dictionary Conversion Fuzzing
|
||||
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
|
||||
@given(
|
||||
|
||||
@@ -62,7 +62,6 @@ def test_fuzz_full_telemetry_packing(charge, charging, rssi, snr, q):
|
||||
@settings(deadline=None)
|
||||
@given(data=st.binary(min_size=0, max_size=2000))
|
||||
def test_fuzz_from_packed_random_bytes(data):
|
||||
# This should never crash
|
||||
try:
|
||||
Telemeter.from_packed(data)
|
||||
except Exception:
|
||||
@@ -97,5 +96,4 @@ def test_fuzz_command_parsing(commands):
|
||||
new_cmd[k] = v
|
||||
processed_commands.append(new_cmd)
|
||||
|
||||
# Just ensure no crash
|
||||
assert len(processed_commands) == len(commands)
|
||||
|
||||
@@ -37,8 +37,6 @@ def test_parse_best_effort_on_failure():
|
||||
[[Fixed Interface]]
|
||||
type = fixed
|
||||
"""
|
||||
# Note: ConfigObj might still parse [[Broken Interface] as a key if not careful,
|
||||
# but the parser should return something.
|
||||
interfaces = InterfaceConfigParser.parse(config_text)
|
||||
assert len(interfaces) >= 1
|
||||
names = [i["name"] for i in interfaces]
|
||||
|
||||
@@ -142,3 +142,107 @@ async def test_specific_node_hash_validation(mock_app):
|
||||
|
||||
await sync_handler(None)
|
||||
mock_app.current_context.message_router.request_messages_from_propagation_node.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_status_includes_sync_storage_and_confirmation_metrics(mock_app):
|
||||
status_handler = next(
|
||||
r.handler
|
||||
for r in mock_app.get_routes()
|
||||
if r.path == "/api/v1/lxmf/propagation-node/status"
|
||||
)
|
||||
sync_handler = next(
|
||||
r.handler
|
||||
for r in mock_app.get_routes()
|
||||
if r.path == "/api/v1/lxmf/propagation-node/sync"
|
||||
)
|
||||
|
||||
mock_app.current_context.message_router.get_outbound_propagation_node.return_value = (
|
||||
b"somehash"
|
||||
)
|
||||
mock_app.current_context.message_router.propagation_transfer_last_result = 8
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
mock_app.current_context.database.messages,
|
||||
"count_lxmf_messages",
|
||||
side_effect=[10, 13],
|
||||
),
|
||||
patch.object(
|
||||
mock_app.current_context.database.messages,
|
||||
"count_lxmf_messages_by_state",
|
||||
side_effect=[2, 4],
|
||||
),
|
||||
):
|
||||
await sync_handler(None)
|
||||
response = await status_handler(None)
|
||||
|
||||
data = json.loads(response.body)["propagation_node_status"]
|
||||
assert data["messages_received"] == 8
|
||||
assert data["messages_stored"] == 3
|
||||
assert data["delivery_confirmations"] == 2
|
||||
assert data["messages_hidden"] == 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_status_metrics_default_to_zero_before_any_sync(mock_app):
|
||||
status_handler = next(
|
||||
r.handler
|
||||
for r in mock_app.get_routes()
|
||||
if r.path == "/api/v1/lxmf/propagation-node/status"
|
||||
)
|
||||
response = await status_handler(None)
|
||||
data = json.loads(response.body)["propagation_node_status"]
|
||||
|
||||
assert data["messages_received"] == 0
|
||||
assert data["messages_stored"] == 0
|
||||
assert data["delivery_confirmations"] == 0
|
||||
assert data["messages_hidden"] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_status_hidden_metric_is_clamped_to_zero(mock_app):
|
||||
status_handler = next(
|
||||
r.handler
|
||||
for r in mock_app.get_routes()
|
||||
if r.path == "/api/v1/lxmf/propagation-node/status"
|
||||
)
|
||||
sync_handler = next(
|
||||
r.handler
|
||||
for r in mock_app.get_routes()
|
||||
if r.path == "/api/v1/lxmf/propagation-node/sync"
|
||||
)
|
||||
|
||||
mock_app.current_context.message_router.get_outbound_propagation_node.return_value = (
|
||||
b"somehash"
|
||||
)
|
||||
mock_app.current_context.message_router.propagation_transfer_last_result = 1
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
mock_app.current_context.database.messages,
|
||||
"count_lxmf_messages",
|
||||
side_effect=[10, 20],
|
||||
),
|
||||
patch.object(
|
||||
mock_app.current_context.database.messages,
|
||||
"count_lxmf_messages_by_state",
|
||||
side_effect=[2, 8],
|
||||
),
|
||||
):
|
||||
await sync_handler(None)
|
||||
response = await status_handler(None)
|
||||
|
||||
data = json.loads(response.body)["propagation_node_status"]
|
||||
assert data["messages_hidden"] == 0
|
||||
|
||||
|
||||
def test_on_lxmf_sending_failed_forwards_context_to_state_update(mock_app):
|
||||
msg = MagicMock(spec=LXMF.LXMessage)
|
||||
msg.state = LXMF.LXMessage.FAILED
|
||||
msg.try_propagation_on_fail = False
|
||||
|
||||
with patch.object(mock_app, "on_lxmf_sending_state_updated") as state_update_mock:
|
||||
mock_app.on_lxmf_sending_failed(msg, context=mock_app.current_context)
|
||||
|
||||
state_update_mock.assert_called_once_with(msg, context=mock_app.current_context)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import unittest
|
||||
import time
|
||||
|
||||
from meshchatx.src.backend.markdown_renderer import MarkdownRenderer
|
||||
|
||||
@@ -22,7 +23,6 @@ class TestMarkdownRenderer(unittest.TestCase):
|
||||
self.assertIn("<pre", rendered)
|
||||
self.assertIn("<code", rendered)
|
||||
self.assertIn("language-python", rendered)
|
||||
# Check for escaped characters
|
||||
self.assertTrue(
|
||||
"print('hello')" in rendered
|
||||
or "print('hello')" in rendered,
|
||||
@@ -67,6 +67,123 @@ class TestMarkdownRenderer(unittest.TestCase):
|
||||
self.assertIn("Para 1", rendered)
|
||||
self.assertIn("Para 2", rendered)
|
||||
|
||||
def test_render_none_and_empty(self):
|
||||
self.assertEqual(MarkdownRenderer.render(None), "")
|
||||
self.assertEqual(MarkdownRenderer.render(""), "")
|
||||
r = MarkdownRenderer.render(" ")
|
||||
self.assertIsInstance(r, str)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
def test_xss_script_tags(self):
|
||||
cases = [
|
||||
"<script>alert(1)</script>",
|
||||
"<SCRIPT>alert(1)</SCRIPT>",
|
||||
"<script src=\"evil.js\"></script>",
|
||||
"</script><script>alert(1)</script>",
|
||||
]
|
||||
for s in cases:
|
||||
r = MarkdownRenderer.render(s)
|
||||
self.assertNotIn("<script>", r, msg=s)
|
||||
self.assertNotIn("</script>", r, msg=s)
|
||||
self.assertIn("<", r, msg=s)
|
||||
|
||||
def test_xss_event_handlers(self):
|
||||
cases = [
|
||||
'<img src="x" onerror="alert(1)">',
|
||||
'<body onload="alert(1)">',
|
||||
'<a href="x" onmouseover="alert(1)">x</a>',
|
||||
'<svg onload="alert(1)">',
|
||||
]
|
||||
for s in cases:
|
||||
r = MarkdownRenderer.render(s)
|
||||
self.assertNotIn("<img", r, msg=s)
|
||||
self.assertNotIn("<body", r, msg=s)
|
||||
self.assertNotIn("<svg", r, msg=s)
|
||||
self.assertNotIn("<a ", r, msg=s)
|
||||
self.assertIn("<", r, msg=s)
|
||||
|
||||
def test_xss_link_href_javascript(self):
|
||||
r = MarkdownRenderer.render("[click](javascript:alert(1))")
|
||||
self.assertNotIn("javascript:", r)
|
||||
self.assertIn('href="#"', r)
|
||||
|
||||
def test_xss_link_href_data(self):
|
||||
r = MarkdownRenderer.render("[click](data:text/html,<script>alert(1)</script>)")
|
||||
self.assertNotIn("data:", r)
|
||||
self.assertIn('href="#"', r)
|
||||
|
||||
def test_xss_link_href_vbscript(self):
|
||||
r = MarkdownRenderer.render("[click](vbscript:msgbox(1))")
|
||||
self.assertNotIn("vbscript:", r)
|
||||
self.assertIn('href="#"', r)
|
||||
|
||||
def test_safe_links_preserved(self):
|
||||
r = MarkdownRenderer.render("[link](https://example.com/path)")
|
||||
self.assertIn('href="https://example.com/path"', r)
|
||||
r = MarkdownRenderer.render("[link](/relative)")
|
||||
self.assertIn('href="/relative"', r)
|
||||
r = MarkdownRenderer.render("[link](#anchor)")
|
||||
self.assertIn('href="#anchor"', r)
|
||||
|
||||
def test_redos_safe_repeated_markers(self):
|
||||
t0 = time.perf_counter()
|
||||
MarkdownRenderer.render("*" * 8000)
|
||||
MarkdownRenderer.render("#" * 8000)
|
||||
MarkdownRenderer.render("`" * 8000)
|
||||
MarkdownRenderer.render("[](" * 2000 + "x" * 2000 + ")" * 2000)
|
||||
elapsed = time.perf_counter() - t0
|
||||
self.assertLess(elapsed, 2.0, "ReDoS or excessive backtracking suspected")
|
||||
|
||||
def test_malformed_unclosed_markdown(self):
|
||||
cases = [
|
||||
"**bold no close",
|
||||
"```\ncode no close",
|
||||
"*italic",
|
||||
"`code",
|
||||
"___under",
|
||||
"[unclosed link](url",
|
||||
"
|
||||
self.assertIsInstance(r, str)
|
||||
self.assertNotIn("<script>", r)
|
||||
|
||||
def test_very_long_input(self):
|
||||
big = "x" * (1024 * 1024)
|
||||
r = MarkdownRenderer.render(big)
|
||||
self.assertIsInstance(r, str)
|
||||
self.assertIn("x", r)
|
||||
|
||||
def test_unicode_and_control_chars(self):
|
||||
cases = [
|
||||
"\x00hello",
|
||||
"hello\x07world",
|
||||
"\u202eRTL",
|
||||
"\ufffd replacement",
|
||||
"# header \n normal",
|
||||
]
|
||||
for s in cases:
|
||||
r = MarkdownRenderer.render(s)
|
||||
self.assertIsInstance(r, str)
|
||||
|
||||
def test_render_returns_string(self):
|
||||
r = MarkdownRenderer.render("Hello **world**")
|
||||
self.assertIsInstance(r, str)
|
||||
self.assertIn("Hello", r)
|
||||
self.assertIn("strong", r)
|
||||
|
||||
def test_message_content_style_input_safe(self):
|
||||
"""Simulate message body (e.g. decoded from LXMF) passed to render; must not crash or emit script."""
|
||||
cases = [
|
||||
b"Hello world".decode("utf-8"),
|
||||
("Hi \ufffd replacement char " * 10).strip(),
|
||||
"\x00\x01\n\t\r",
|
||||
"<script>alert(1)</script> normal text",
|
||||
"**bold** and [link](javascript:x) end",
|
||||
"\u202eRTL override",
|
||||
]
|
||||
for s in cases:
|
||||
r = MarkdownRenderer.render(s)
|
||||
self.assertIsInstance(r, str)
|
||||
self.assertNotIn("<script>", r)
|
||||
self.assertNotIn("javascript:", r)
|
||||
|
||||
@@ -290,4 +290,4 @@ def test_on_lxmf_sending_failed_no_propagation(mock_app):
|
||||
mock_app.on_lxmf_sending_state_updated = MagicMock()
|
||||
|
||||
mock_app.on_lxmf_sending_failed(mock_msg)
|
||||
mock_app.on_lxmf_sending_state_updated.assert_called_once_with(mock_msg)
|
||||
mock_app.on_lxmf_sending_state_updated.assert_called_once_with(mock_msg, context=None)
|
||||
|
||||
@@ -264,9 +264,6 @@ async def test_notifications_api(mock_app):
|
||||
# But it's defined as a nested function.
|
||||
# Alternatively, we can test the DAOs and meshchat.py logic that the handler uses.
|
||||
|
||||
# For now, let's verify that system notifications are correctly combined with LXMF messages.
|
||||
# This is done in notifications_get.
|
||||
|
||||
# Let's test a spike of notifications
|
||||
for i in range(100):
|
||||
mock_app.database.misc.add_notification(
|
||||
|
||||
@@ -90,7 +90,6 @@ class TestPerformanceBottlenecks(unittest.TestCase):
|
||||
app_data = b"app_data"
|
||||
packet_hash = b"packet_hash"
|
||||
|
||||
# This is the synchronous part that could be a bottleneck
|
||||
self.announce_manager.upsert_announce(
|
||||
self.reticulum_mock,
|
||||
identities[i],
|
||||
|
||||
@@ -171,7 +171,6 @@ def test_parse_bool_query_param(val):
|
||||
|
||||
@given(data=st.binary())
|
||||
def test_parse_lxmf_display_name_robustness(data):
|
||||
# This should never crash
|
||||
try:
|
||||
parse_lxmf_display_name(data)
|
||||
except Exception as e:
|
||||
@@ -180,7 +179,6 @@ def test_parse_lxmf_display_name_robustness(data):
|
||||
|
||||
@given(data=st.binary())
|
||||
def test_parse_lxmf_propagation_node_app_data_robustness(data):
|
||||
# This should never crash
|
||||
try:
|
||||
result = parse_lxmf_propagation_node_app_data(data)
|
||||
if result is not None:
|
||||
@@ -226,7 +224,6 @@ def test_interface_config_parser_best_effort_property(names, keys, values):
|
||||
|
||||
@given(data=st.binary())
|
||||
def test_parse_lxmf_stamp_cost_robustness(data):
|
||||
# This should never crash
|
||||
try:
|
||||
parse_lxmf_stamp_cost(data)
|
||||
except Exception as e:
|
||||
@@ -235,7 +232,6 @@ def test_parse_lxmf_stamp_cost_robustness(data):
|
||||
|
||||
@given(name=st.text())
|
||||
def test_parse_nomadnetwork_node_display_name_robustness(name):
|
||||
# This should never crash
|
||||
try:
|
||||
parse_nomadnetwork_node_display_name(name)
|
||||
except Exception as e:
|
||||
@@ -244,7 +240,6 @@ def test_parse_nomadnetwork_node_display_name_robustness(name):
|
||||
|
||||
@given(packed=st.binary())
|
||||
def test_telemeter_from_packed_robustness(packed):
|
||||
# This should never crash
|
||||
try:
|
||||
Telemeter.from_packed(packed)
|
||||
except Exception as e:
|
||||
@@ -444,8 +439,6 @@ def test_markdown_renderer_headers(content):
|
||||
assert result.startswith("<h1")
|
||||
assert result.endswith("</h1>")
|
||||
|
||||
# If the content doesn't contain markdown special chars, we can expect it to be there escaped
|
||||
# This is a safer assertion for property-based testing
|
||||
if not any(c in content for c in "*_~`[]()"):
|
||||
assert html.escape(content) in result
|
||||
|
||||
@@ -852,13 +845,11 @@ def test_lxmf_utils_conversions_robustness(
|
||||
hex_str=st.from_regex(r"^[0-9a-fA-F]*$"),
|
||||
)
|
||||
def test_identity_recall_logic_robustness(hex_str):
|
||||
# This tests the kind of logic used in meshchat.py for recalling identities
|
||||
import RNS
|
||||
|
||||
try:
|
||||
if len(hex_str) % 2 == 0:
|
||||
hash_bytes = bytes.fromhex(hex_str)
|
||||
# Just ensure RNS doesn't crash on random bytes
|
||||
RNS.Identity.recall(hash_bytes)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -31,7 +31,6 @@ def test_fuzz_reply_detection_no_crash(content):
|
||||
mock_msg.snr = 0
|
||||
mock_msg.q = 0
|
||||
|
||||
# This will trigger the detection logic
|
||||
mesh_chat.db_upsert_lxmf_message(mock_msg)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import base64
|
||||
import json
|
||||
import unittest
|
||||
|
||||
|
||||
class TestBase64DecodeRisky(unittest.TestCase):
|
||||
def test_invalid_base64_does_not_crash_display_name(self):
|
||||
try:
|
||||
from meshchatx.src.backend.meshchat_utils import parse_lxmf_display_name
|
||||
except ImportError:
|
||||
self.skipTest("meshchat_utils not importable")
|
||||
for bad in ["!!!", "\x00\x01", "a" * 10000, " ", "=", "===", "a\nb\tc", "\xff\xfe"]:
|
||||
result = parse_lxmf_display_name(bad, default_value="Fallback")
|
||||
self.assertIsInstance(result, str)
|
||||
|
||||
def test_invalid_base64_does_not_crash_propagation_node_app_data(self):
|
||||
try:
|
||||
from meshchatx.src.backend.meshchat_utils import (
|
||||
parse_lxmf_propagation_node_app_data,
|
||||
)
|
||||
except ImportError:
|
||||
self.skipTest("meshchat_utils not importable")
|
||||
for bad in ["!!!", "", "a" * 5000]:
|
||||
try:
|
||||
parse_lxmf_propagation_node_app_data(bad)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def test_invalid_base64_does_not_crash_stamp_cost(self):
|
||||
try:
|
||||
from meshchatx.src.backend.meshchat_utils import parse_lxmf_stamp_cost
|
||||
except ImportError:
|
||||
self.skipTest("meshchat_utils not importable")
|
||||
for bad in ["!!!", "", "x" * 10000]:
|
||||
try:
|
||||
parse_lxmf_stamp_cost(bad)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def test_base64_decode_direct_invalid_chars(self):
|
||||
for s in ["a\x00b", "a--b", "a/b+c", " " * 1000]:
|
||||
try:
|
||||
base64.b64decode(s, validate=False)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
base64.b64decode("!!!", validate=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class TestJsonLoadsRisky(unittest.TestCase):
|
||||
def test_deeply_nested_json_does_not_crash_message_dict(self):
|
||||
try:
|
||||
from meshchatx.src.backend.lxmf_utils import convert_db_lxmf_message_to_dict
|
||||
except ImportError:
|
||||
self.skipTest("lxmf_utils not importable")
|
||||
deep = "x"
|
||||
for _ in range(200):
|
||||
deep = '{"a":' + deep + "}"
|
||||
db_msg = {
|
||||
"id": 1,
|
||||
"hash": "h",
|
||||
"source_hash": "s",
|
||||
"destination_hash": "d",
|
||||
"is_incoming": True,
|
||||
"state": "delivered",
|
||||
"progress": 100.0,
|
||||
"method": "direct",
|
||||
"delivery_attempts": 0,
|
||||
"next_delivery_attempt_at": None,
|
||||
"title": "t",
|
||||
"content": "c",
|
||||
"fields": deep,
|
||||
"timestamp": 123.0,
|
||||
"rssi": -50,
|
||||
"snr": 10,
|
||||
"quality": 100,
|
||||
"is_spam": False,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
try:
|
||||
convert_db_lxmf_message_to_dict(db_msg)
|
||||
except (RecursionError, OverflowError):
|
||||
pass
|
||||
|
||||
def test_prototype_pollution_keys_in_fields_do_not_crash(self):
|
||||
try:
|
||||
from meshchatx.src.backend.lxmf_utils import convert_db_lxmf_message_to_dict
|
||||
except ImportError:
|
||||
self.skipTest("lxmf_utils not importable")
|
||||
db_msg = {
|
||||
"id": 1,
|
||||
"hash": "h",
|
||||
"source_hash": "s",
|
||||
"destination_hash": "d",
|
||||
"is_incoming": True,
|
||||
"state": "delivered",
|
||||
"progress": 100.0,
|
||||
"method": "direct",
|
||||
"delivery_attempts": 0,
|
||||
"next_delivery_attempt_at": None,
|
||||
"title": "t",
|
||||
"content": "c",
|
||||
"fields": '{"__proto__":{"x":1},"constructor":{"prototype":{"y":1}}}',
|
||||
"timestamp": 123.0,
|
||||
"rssi": -50,
|
||||
"snr": 10,
|
||||
"quality": 100,
|
||||
"is_spam": False,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
result = convert_db_lxmf_message_to_dict(db_msg)
|
||||
self.assertIsInstance(result, dict)
|
||||
|
||||
def test_huge_fields_string_does_not_hang(self):
|
||||
try:
|
||||
from meshchatx.src.backend.lxmf_utils import convert_db_lxmf_message_to_dict
|
||||
except ImportError:
|
||||
self.skipTest("lxmf_utils not importable")
|
||||
db_msg = {
|
||||
"id": 1,
|
||||
"hash": "h",
|
||||
"source_hash": "s",
|
||||
"destination_hash": "d",
|
||||
"is_incoming": True,
|
||||
"state": "delivered",
|
||||
"progress": 100.0,
|
||||
"method": "direct",
|
||||
"delivery_attempts": 0,
|
||||
"next_delivery_attempt_at": None,
|
||||
"title": "t",
|
||||
"content": "c",
|
||||
"fields": '{"k":"' + "v" * 50000 + '"}',
|
||||
"timestamp": 123.0,
|
||||
"rssi": -50,
|
||||
"snr": 10,
|
||||
"quality": 100,
|
||||
"is_spam": False,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
try:
|
||||
convert_db_lxmf_message_to_dict(db_msg)
|
||||
except (MemoryError, json.JSONDecodeError):
|
||||
pass
|
||||
|
||||
def test_invalid_json_fields_returns_safe_dict(self):
|
||||
try:
|
||||
from meshchatx.src.backend.lxmf_utils import convert_db_lxmf_message_to_dict
|
||||
except ImportError:
|
||||
self.skipTest("lxmf_utils not importable")
|
||||
db_msg = {
|
||||
"id": 1,
|
||||
"hash": "h",
|
||||
"source_hash": "s",
|
||||
"destination_hash": "d",
|
||||
"is_incoming": True,
|
||||
"state": "delivered",
|
||||
"progress": 100.0,
|
||||
"method": "direct",
|
||||
"delivery_attempts": 0,
|
||||
"next_delivery_attempt_at": None,
|
||||
"title": "t",
|
||||
"content": "c",
|
||||
"fields": "not json at all {{{",
|
||||
"timestamp": 123.0,
|
||||
"rssi": -50,
|
||||
"snr": 10,
|
||||
"quality": 100,
|
||||
"is_spam": False,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
result = convert_db_lxmf_message_to_dict(db_msg)
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn("fields", result)
|
||||
|
||||
|
||||
class TestPathAndStringRisky(unittest.TestCase):
|
||||
def test_interface_config_with_null_byte(self):
|
||||
try:
|
||||
from meshchatx.src.backend.interface_config_parser import (
|
||||
InterfaceConfigParser,
|
||||
)
|
||||
except ImportError:
|
||||
self.skipTest("interface_config_parser not importable")
|
||||
try:
|
||||
InterfaceConfigParser.parse("[section]\nname\x00=value")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def test_interface_config_very_long_line(self):
|
||||
try:
|
||||
from meshchatx.src.backend.interface_config_parser import (
|
||||
InterfaceConfigParser,
|
||||
)
|
||||
except ImportError:
|
||||
self.skipTest("interface_config_parser not importable")
|
||||
try:
|
||||
InterfaceConfigParser.parse("[x]\nkey=" + "v" * 100000)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -355,7 +355,6 @@ def test_telephone_callback_fuzzing(mock_app, caller_id_bytes):
|
||||
)
|
||||
def test_message_dao_upsert_fuzzing(mock_app, data):
|
||||
"""Fuzz MessageDAO.upsert_lxmf_message with varied dictionary data."""
|
||||
# This should not raise SQL errors or crash
|
||||
mock_app.database.messages.upsert_lxmf_message(data)
|
||||
|
||||
|
||||
|
||||
@@ -40,9 +40,4 @@ def test_telemeter_unpack_location_robustness():
|
||||
|
||||
|
||||
def test_sideband_request_format_compatibility():
|
||||
# Sideband telemetry request command is 0x01
|
||||
# It can be a simple int 0x01 or a dict {0x01: timebase}
|
||||
|
||||
# This test is more about the logic in on_lxmf_delivery,
|
||||
# but we can verify our assumptions about command structure here if needed.
|
||||
pass
|
||||
|
||||
@@ -65,7 +65,6 @@ async def test_process_incoming_telemetry_single(mock_app):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_incoming_telemetry_stream(mock_app):
|
||||
# This simulates receiving a telemetry stream (e.g. from Sideband collector)
|
||||
entries = [
|
||||
(
|
||||
"peer1",
|
||||
|
||||
Reference in New Issue
Block a user