""" 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