aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_daemon.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_daemon.py')
-rw-r--r--packages/meshbay-node/tests/test_daemon.py171
1 files changed, 171 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_daemon.py b/packages/meshbay-node/tests/test_daemon.py
new file mode 100644
index 0000000..e74f7fc
--- /dev/null
+++ b/packages/meshbay-node/tests/test_daemon.py
@@ -0,0 +1,171 @@
+"""
+Integration test: Node daemon wires all components correctly.
+
+Phase 11 — verifies that NodeDaemon creates chat stores, WebRTC transport,
+and shuts down cleanly. Hub interaction is mocked.
+"""
+
+import asyncio
+import os
+
+import pytest
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from unittest.mock import AsyncMock, MagicMock, patch
+
+from meshbay_common.crypto import generate_gek
+from meshbay_node.config import Config, HubConfig, NodeConfig, GroupConfig, KeystoreConfig
+from meshbay_node.daemon import NodeDaemon
+
+
+@pytest.fixture
+def sk_hub():
+ return Ed25519PrivateKey.generate()
+
+
+@pytest.fixture
+def hub_pk_pem(sk_hub):
+ return sk_hub.public_key().public_bytes(
+ serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo)
+
+
+@pytest.fixture
+def gek():
+ return generate_gek()
+
+
+@pytest.fixture
+def shared_dir(tmp_path):
+ d = tmp_path / "shared"
+ d.mkdir()
+ (d / "test.bin").write_bytes(os.urandom(2048))
+ (d / "hello.txt").write_bytes(b"hello daemon test " * 50)
+ return d
+
+
+@pytest.fixture
+def node_config(tmp_path, shared_dir):
+ return Config(
+ hub=HubConfig(url="http://localhost:9999", username="testuser", password="testpass"),
+ node=NodeConfig(port=29000, quic_port=29010, http_port=29001, ui_port=28000),
+ groups=[GroupConfig(
+ id="g" * 32,
+ name="test-group",
+ shared_dir=str(shared_dir),
+ visibility="private",
+ port=29000,
+ quic_port=29010,
+ http_port=29001,
+ )],
+ keystore=KeystoreConfig(path=tmp_path / "keystore.enc"),
+ data_dir=tmp_path / "data",
+ )
+
+
+@pytest.mark.asyncio
+async def test_daemon_creates_chat_store(tmp_path, node_config, gek, hub_pk_pem):
+ """Daemon creates ChatStore for each group and shuts down cleanly."""
+ daemon = NodeDaemon(node_config)
+
+ sk_node = Ed25519PrivateKey.generate()
+ mock_keys = MagicMock()
+ mock_keys.sk_ed25519 = sk_node
+ mock_keys.pk_ed25519_b64 = "test"
+ mock_keys.pk_x25519_b64 = "test"
+
+ mock_session = MagicMock()
+ mock_session.node_id = "node123"
+ mock_session.user_id = "user123"
+ mock_session.hub_pk_pem = hub_pk_pem
+
+ with patch("meshbay_node.daemon.load_or_create_keystore", return_value=mock_keys), \
+ patch("meshbay_node.daemon.HubClient") as MockHub:
+
+ hub_instance = AsyncMock()
+ hub_instance.startup = AsyncMock(return_value=mock_session)
+ hub_instance.fetch_gek = AsyncMock(return_value=gek)
+ hub_instance.maintain_ws = AsyncMock()
+ hub_instance.send_ws = AsyncMock()
+ hub_instance._ws = None
+ hub_instance.close = AsyncMock()
+ hub_instance.__aenter__ = AsyncMock(return_value=hub_instance)
+ hub_instance.__aexit__ = AsyncMock(return_value=False)
+ MockHub.return_value = hub_instance
+
+ shutdown_event = asyncio.Event()
+
+ async def mock_maintain_ws(**kwargs):
+ await shutdown_event.wait()
+
+ hub_instance.maintain_ws = mock_maintain_ws
+
+ async def run_daemon():
+ with patch("signal.SIGINT", 2), \
+ patch("signal.SIGTERM", 15):
+ try:
+ await asyncio.wait_for(daemon.run(), timeout=5)
+ except (asyncio.TimeoutError, Exception):
+ pass
+
+ task = asyncio.create_task(run_daemon())
+ await asyncio.sleep(1)
+
+ assert daemon._state["status"] == "running"
+ group_id = "g" * 32
+ assert group_id in daemon._chat_stores
+ assert daemon._chat_stores[group_id]._db is not None
+
+ if daemon._webrtc:
+ assert "chat_store" in daemon._webrtc._ctx
+ assert "hub_ws" in daemon._webrtc._ctx
+ assert "node_user_id" in daemon._webrtc._ctx
+ assert daemon._webrtc._ctx["node_user_id"] == "user123"
+
+ shutdown_event.set()
+ await daemon._shutdown()
+ task.cancel()
+ try:
+ await task
+ except (asyncio.CancelledError, Exception):
+ pass
+
+ for store in daemon._chat_stores.values():
+ assert store._db is None
+
+
+@pytest.mark.asyncio
+async def test_daemon_no_groups_exits(tmp_path):
+ """Daemon with no valid groups exits cleanly."""
+ config = Config(
+ hub=HubConfig(url="http://localhost:9999", username="testuser", password="testpass"),
+ node=NodeConfig(),
+ groups=[GroupConfig(id="", name="empty", shared_dir="")],
+ keystore=KeystoreConfig(path=tmp_path / "keystore.enc"),
+ data_dir=tmp_path / "data",
+ )
+ daemon = NodeDaemon(config)
+
+ sk_node = Ed25519PrivateKey.generate()
+ mock_keys = MagicMock()
+ mock_keys.sk_ed25519 = sk_node
+ mock_keys.pk_ed25519_b64 = "test"
+
+ mock_session = MagicMock()
+ mock_session.node_id = "node123"
+ mock_session.user_id = "user123"
+ mock_session.hub_pk_pem = b"pem"
+
+ with patch("meshbay_node.daemon.load_or_create_keystore", return_value=mock_keys), \
+ patch("meshbay_node.daemon.HubClient") as MockHub:
+
+ hub_instance = AsyncMock()
+ hub_instance.startup = AsyncMock(return_value=mock_session)
+ hub_instance.close = AsyncMock()
+ hub_instance.__aenter__ = AsyncMock(return_value=hub_instance)
+ hub_instance.__aexit__ = AsyncMock(return_value=False)
+ MockHub.return_value = hub_instance
+
+ await daemon.run()
+
+ assert daemon._state["status"] == "starting"
+ assert len(daemon._chat_stores) == 0