interface discovery, folders for messages, map nodes from discovery, maintenance tools.

This commit is contained in:
2026-01-05 17:38:52 -06:00
parent 30cab64101
commit 666c90875a
26 changed files with 3272 additions and 294 deletions

View File

@@ -0,0 +1,85 @@
import os
import shutil
import tempfile
import base64
import unittest
from unittest.mock import MagicMock, patch
from meshchatx.src.backend.identity_manager import IdentityManager
class TestIdentityRestore(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.mkdtemp()
self.identity_manager = IdentityManager(self.temp_dir)
def tearDown(self):
shutil.rmtree(self.temp_dir)
@patch("RNS.Identity")
@patch("meshchatx.src.backend.identity_manager.DatabaseProvider")
@patch("meshchatx.src.backend.identity_manager.DatabaseSchema")
def test_restore_identity_from_bytes(
self, mock_schema, mock_provider, mock_rns_identity
):
# Setup mock identity
mock_id_instance = MagicMock()
mock_id_instance.hash = b"test_hash_32_bytes_long_01234567"
mock_id_instance.get_private_key.return_value = b"test_private_key"
mock_rns_identity.from_bytes.return_value = mock_id_instance
identity_bytes = b"some_identity_bytes"
result = self.identity_manager.restore_identity_from_bytes(identity_bytes)
identity_hash = mock_id_instance.hash.hex()
self.assertEqual(result["hash"], identity_hash)
self.assertEqual(result["display_name"], "Restored Identity")
# Verify files were created
identity_dir = os.path.join(self.temp_dir, "identities", identity_hash)
self.assertTrue(os.path.exists(identity_dir))
self.assertTrue(os.path.exists(os.path.join(identity_dir, "identity")))
self.assertTrue(os.path.exists(os.path.join(identity_dir, "metadata.json")))
# Verify private key was written
with open(os.path.join(identity_dir, "identity"), "rb") as f:
self.assertEqual(f.read(), b"test_private_key")
@patch("RNS.Identity")
@patch("meshchatx.src.backend.identity_manager.DatabaseProvider")
@patch("meshchatx.src.backend.identity_manager.DatabaseSchema")
def test_restore_identity_from_base32(
self, mock_schema, mock_provider, mock_rns_identity
):
# Setup mock identity
mock_id_instance = MagicMock()
mock_id_instance.hash = b"test_hash_32_bytes_long_01234567"
mock_id_instance.get_private_key.return_value = b"test_private_key"
mock_rns_identity.from_bytes.return_value = mock_id_instance
identity_bytes = b"some_identity_bytes"
base32_value = base64.b32encode(identity_bytes).decode("utf-8")
result = self.identity_manager.restore_identity_from_base32(base32_value)
identity_hash = mock_id_instance.hash.hex()
self.assertEqual(result["hash"], identity_hash)
# Verify from_bytes was called with the decoded bytes
mock_rns_identity.from_bytes.assert_called_with(identity_bytes)
@patch("RNS.Identity")
def test_restore_identity_invalid_bytes(self, mock_rns_identity):
mock_rns_identity.from_bytes.return_value = None
with self.assertRaises(ValueError) as cm:
self.identity_manager.restore_identity_from_bytes(b"invalid")
self.assertIn("Could not load identity from bytes", str(cm.exception))
@patch("RNS.Identity")
def test_restore_identity_invalid_base32(self, mock_rns_identity):
with self.assertRaises(ValueError) as cm:
self.identity_manager.restore_identity_from_base32("invalid-base32-!!!")
self.assertIn("Invalid base32 identity", str(cm.exception))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,64 @@
import unittest
from unittest.mock import MagicMock
from meshchatx.src.backend.database.messages import MessageDAO
from meshchatx.src.backend.database.announces import AnnounceDAO
from meshchatx.src.backend.database.misc import MiscDAO
class TestMaintenance(unittest.TestCase):
def setUp(self):
self.provider = MagicMock()
self.messages_dao = MessageDAO(self.provider)
self.announces_dao = AnnounceDAO(self.provider)
self.misc_dao = MiscDAO(self.provider)
def test_delete_all_lxmf_messages(self):
self.messages_dao.delete_all_lxmf_messages()
self.assertEqual(self.provider.execute.call_count, 2)
calls = self.provider.execute.call_args_list
self.assertIn("DELETE FROM lxmf_messages", calls[0][0][0])
self.assertIn("DELETE FROM lxmf_conversation_read_state", calls[1][0][0])
def test_delete_all_announces(self):
# Test without aspect
self.announces_dao.delete_all_announces()
self.provider.execute.assert_called_with("DELETE FROM announces")
# Test with aspect
self.announces_dao.delete_all_announces(aspect="test_aspect")
self.provider.execute.assert_called_with(
"DELETE FROM announces WHERE aspect = ?", ("test_aspect",)
)
def test_delete_all_favourites(self):
# Test without aspect
self.announces_dao.delete_all_favourites()
self.provider.execute.assert_called_with("DELETE FROM favourite_destinations")
# Test with aspect
self.announces_dao.delete_all_favourites(aspect="test_aspect")
self.provider.execute.assert_called_with(
"DELETE FROM favourite_destinations WHERE aspect = ?", ("test_aspect",)
)
def test_delete_archived_pages(self):
self.misc_dao.delete_archived_pages()
self.provider.execute.assert_called_with("DELETE FROM archived_pages")
def test_upsert_lxmf_message(self):
msg_data = {
"hash": "test_hash",
"source_hash": "source",
"destination_hash": "dest",
"peer_hash": "peer",
"content": "hello",
}
self.messages_dao.upsert_lxmf_message(msg_data)
self.provider.execute.assert_called()
args, _ = self.provider.execute.call_args
self.assertIn("INSERT INTO lxmf_messages", args[0])
self.assertIn("ON CONFLICT(hash) DO UPDATE SET", args[0])
if __name__ == "__main__":
unittest.main()

View File

@@ -22,10 +22,14 @@ class TestMessageHandler(unittest.TestCase):
def test_delete_conversation(self):
self.handler.delete_conversation("local", "dest")
self.db.provider.execute.assert_called()
args, kwargs = self.db.provider.execute.call_args
self.assertIn("DELETE FROM lxmf_messages", args[0])
self.assertIn("dest", args[1])
self.assertEqual(self.db.provider.execute.call_count, 2)
call_args_list = self.db.provider.execute.call_args_list
first_call_args, _ = call_args_list[0]
second_call_args, _ = call_args_list[1]
self.assertIn("DELETE FROM lxmf_messages", first_call_args[0])
self.assertIn("dest", first_call_args[1])
self.assertIn("DELETE FROM lxmf_conversation_folders", second_call_args[0])
self.assertIn("dest", second_call_args[1])
if __name__ == "__main__":

View File

@@ -1,8 +1,18 @@
import { mount } from "@vue/test-utils";
import { describe, it, expect, vi } from "vitest";
import { describe, it, expect, vi, beforeEach } from "vitest";
import MessagesSidebar from "@/components/messages/MessagesSidebar.vue";
describe("MessagesSidebar.vue", () => {
beforeEach(() => {
// Mock localStorage
global.localStorage = {
getItem: vi.fn(() => null),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
};
});
const defaultProps = {
peers: {},
conversations: [],
@@ -38,11 +48,13 @@ describe("MessagesSidebar.vue", () => {
const wrapper = mountMessagesSidebar({ conversations });
const nameElement = wrapper.find(".truncate");
const nameElement = wrapper.find(".conversation-item .truncate");
expect(nameElement.exists()).toBe(true);
expect(nameElement.text()).toContain("Long Name");
const previewElement = wrapper.findAll(".truncate").find((el) => el.text().includes("Message"));
const previewElement = wrapper
.findAll(".conversation-item .truncate")
.find((el) => el.text().includes("Message"));
expect(previewElement.exists()).toBe(true);
});
@@ -60,7 +72,7 @@ describe("MessagesSidebar.vue", () => {
expect(scrollContainer.exists()).toBe(true);
expect(scrollContainer.classes()).toContain("overflow-y-auto");
const conversationItems = wrapper.findAll("div.overflow-y-auto .cursor-pointer");
const conversationItems = wrapper.findAll(".conversation-item");
expect(conversationItems.length).toBe(100);
});

View File

@@ -106,7 +106,7 @@ describe("UI Performance and Memory Tests", () => {
`Rendered ${numConvs} conversations in ${renderTime.toFixed(2)}ms, Memory growth: ${memGrowth.toFixed(2)}MB`
);
expect(wrapper.findAll(".flex.cursor-pointer").length).toBe(numConvs);
expect(wrapper.findAll(".conversation-item").length).toBe(numConvs);
expect(renderTime).toBeLessThan(5000);
expect(memGrowth).toBeLessThan(200); // Adjusted for JSDOM/Node.js overhead with 2000 items
});