| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- # -*- coding: utf-8 -*-
- """
- Unit-Tests für trixy_core.network.
- """
- import pytest
- from dataclasses import dataclass
- from datetime import datetime
- from pathlib import Path
- from trixy_core.network.protocol import (
- TrixyProtocol, ProtocolFlags, ProtocolMessage,
- MAGIC, COMMAND_PING, COMMAND_PONG, COMMAND_NOOP, COMMAND_HELLO
- )
- from trixy_core.network.encryption import TrixyEncryption
- from trixy_core.network.cmd.base import CommandMessage
- from trixy_core.network.cmd.satellite import (
- SatelliteConnect, SatelliteDisconnect,
- SatelliteAccepted, SatelliteDenied
- )
- from trixy_core.network.cmd.wakeword import (
- WakewordDetected, WakewordSelected, WakewordAbort
- )
- from trixy_core.network.cmd.recording import (
- RecordingStart, RecordingStop, RecordingDone
- )
- from trixy_core.network.cmd.system import (
- SystemReboot, SystemUpdate, SystemStatus
- )
- class TestProtocolFlags:
- """Tests für ProtocolFlags."""
- def test_flag_values(self):
- """Testet die Flag-Werte."""
- assert ProtocolFlags.NONE == 0
- assert ProtocolFlags.COMPRESSED == 1
- assert ProtocolFlags.ENCRYPTED == 2
- assert ProtocolFlags.JSON == 4
- assert ProtocolFlags.ACK_REQUIRED == 8
- def test_flag_combination(self):
- """Testet Flag-Kombinationen."""
- flags = ProtocolFlags.COMPRESSED | ProtocolFlags.ENCRYPTED
- assert flags & ProtocolFlags.COMPRESSED
- assert flags & ProtocolFlags.ENCRYPTED
- assert not (flags & ProtocolFlags.JSON)
- class TestProtocolMessage:
- """Tests für ProtocolMessage."""
- def test_message_creation(self):
- """Testet die Nachrichtenerstellung."""
- msg = ProtocolMessage(
- class_name="TestMessage",
- data={"key": "value"},
- flags=ProtocolFlags.JSON
- )
- assert msg.class_name == "TestMessage"
- assert msg.data == {"key": "value"}
- assert msg.flags == ProtocolFlags.JSON
- def test_header_size(self):
- """Testet die Header-Größe."""
- # Magic(4) + Version(2) + Timestamp(8) + Flags(4) + Checksum(16) + ClassNameLen(2)
- assert ProtocolMessage.HEADER_SIZE == 36
- class TestTrixyProtocol:
- """Tests für TrixyProtocol."""
- @pytest.fixture
- def protocol(self):
- """Erstellt ein TrixyProtocol."""
- return TrixyProtocol()
- @pytest.fixture
- def protocol_with_encryption(self):
- """Erstellt ein TrixyProtocol mit Verschlüsselung."""
- encryption = TrixyEncryption()
- return TrixyProtocol(encryption=encryption)
- def test_serialize_deserialize_basic(self, protocol):
- """Testet einfache Serialisierung/Deserialisierung mit CommandMessage."""
- # Verwende CommandMessage (global definiert, pickle-fähig)
- original = SatelliteConnect(
- mac_address="AA:BB:CC:DD:EE:FF",
- room="test",
- alias="TestDevice"
- )
- serialized = protocol.serialize(original)
- deserialized = protocol.deserialize(serialized)
- assert deserialized.class_name == "SatelliteConnect"
- assert deserialized.data.mac_address == "AA:BB:CC:DD:EE:FF"
- assert deserialized.data.room == "test"
- def test_serialize_with_json_flag(self, protocol):
- """Testet Serialisierung mit JSON-Flag."""
- # Verwende einfaches Dictionary für JSON-Test
- original = {"mac_address": "BB:CC:DD:EE:FF:00", "room": "wohnzimmer"}
- serialized = protocol.serialize(original, ProtocolFlags.JSON)
- deserialized = protocol.deserialize(serialized)
- assert deserialized.flags & ProtocolFlags.JSON
- assert deserialized.data["mac_address"] == "BB:CC:DD:EE:FF:00"
- def test_serialize_binary_data(self, protocol):
- """Testet Serialisierung von Binärdaten."""
- binary = b"\x00\x01\x02\x03\x04"
- serialized = protocol.serialize(binary, ProtocolFlags.BINARY)
- deserialized = protocol.deserialize(serialized)
- assert deserialized.flags & ProtocolFlags.BINARY
- assert deserialized.data == binary
- def test_checksum_validation(self, protocol):
- """Testet die Prüfsummenvalidierung."""
- original = SystemStatus(status="running", uptime_seconds=100.0)
- serialized = protocol.serialize(original)
- # Manipuliere Daten
- corrupted = bytearray(serialized)
- corrupted[-1] ^= 0xFF
- with pytest.raises(ValueError, match="Prüfsummenfehler"):
- protocol.deserialize(bytes(corrupted))
- def test_hard_commands(self, protocol):
- """Testet Hard-coded Befehle."""
- assert protocol.create_ping() == COMMAND_PING
- assert protocol.create_pong() == COMMAND_PONG
- assert protocol.create_noop() == COMMAND_NOOP
- assert protocol.create_hello() == COMMAND_HELLO
- def test_is_hard_command(self, protocol):
- """Testet is_hard_command."""
- assert protocol.is_hard_command(COMMAND_PING)
- assert protocol.is_hard_command(COMMAND_PONG)
- assert not protocol.is_hard_command(b"NOTACMD!")
- def test_get_hard_command(self, protocol):
- """Testet get_hard_command."""
- assert protocol.get_hard_command(COMMAND_PING) == "TRXIPING"
- assert protocol.get_hard_command(b"NOTACMD!") is None
- def test_register_class(self, protocol):
- """Testet die Klassenregistrierung."""
- @dataclass
- class RegisteredClass:
- value: str = "registered"
- protocol.register_class(RegisteredClass)
- assert "RegisteredClass" in protocol._registered_classes
- def test_invalid_magic_raises_error(self, protocol):
- """Testet, dass ungültige Magic-Number Fehler wirft."""
- invalid_data = b"XXXX" + b"\x00" * 50
- with pytest.raises(ValueError, match="Ungültige Magic Number"):
- protocol.deserialize(invalid_data)
- def test_serialization_with_encryption(self, protocol_with_encryption):
- """Testet Serialisierung mit Verschlüsselung."""
- original = SatelliteConnect(
- mac_address="AA:BB:CC:DD:EE:FF",
- room="secret_room",
- alias="SecretDevice"
- )
- flags = ProtocolFlags.ENCRYPTED | ProtocolFlags.PICKLE
- serialized = protocol_with_encryption.serialize(original, flags)
- deserialized = protocol_with_encryption.deserialize(serialized)
- assert deserialized.data.room == "secret_room"
- class TestTrixyEncryption:
- """Tests für TrixyEncryption."""
- @pytest.fixture
- def encryption(self):
- """Erstellt eine TrixyEncryption-Instanz."""
- return TrixyEncryption()
- def test_key_generation(self):
- """Testet die Schlüsselgenerierung."""
- key = TrixyEncryption.generate_key()
- assert len(key) == 32
- def test_initialization_with_key(self):
- """Testet Initialisierung mit Schlüssel."""
- key = TrixyEncryption.generate_key()
- enc = TrixyEncryption(key)
- assert enc.key == key
- def test_initialization_generates_key(self):
- """Testet, dass Initialisierung Schlüssel generiert."""
- enc = TrixyEncryption()
- assert len(enc.key) == 32
- def test_invalid_key_length_raises_error(self):
- """Testet, dass ungültige Schlüssellänge Fehler wirft."""
- with pytest.raises(ValueError, match="32 Bytes"):
- TrixyEncryption(b"short")
- def test_encrypt_decrypt(self, encryption):
- """Testet Verschlüsselung und Entschlüsselung."""
- plaintext = b"Hello, World!"
- ciphertext = encryption.encrypt(plaintext)
- decrypted = encryption.decrypt(ciphertext)
- assert decrypted == plaintext
- assert ciphertext != plaintext
- def test_encrypt_with_associated_data(self, encryption):
- """Testet Verschlüsselung mit zusätzlichen Daten."""
- plaintext = b"Secret message"
- aad = b"Additional authenticated data"
- ciphertext = encryption.encrypt(plaintext, aad)
- decrypted = encryption.decrypt(ciphertext, aad)
- assert decrypted == plaintext
- def test_decrypt_with_wrong_aad_fails(self, encryption):
- """Testet, dass Entschlüsselung mit falschen AAD fehlschlägt."""
- plaintext = b"Secret"
- ciphertext = encryption.encrypt(plaintext, b"correct")
- with pytest.raises(ValueError, match="Entschlüsselung fehlgeschlagen"):
- encryption.decrypt(ciphertext, b"wrong")
- def test_save_load_key(self, encryption, temp_dir):
- """Testet Speichern und Laden von Schlüsseln."""
- key_path = temp_dir / "encryption.key"
- encryption.save_key(key_path)
- loaded = TrixyEncryption.load_key(key_path)
- assert loaded.key == encryption.key
- def test_load_or_create_creates_new(self, temp_dir):
- """Testet load_or_create für neue Schlüssel."""
- key_path = temp_dir / "new.key"
- enc = TrixyEncryption.load_or_create(key_path)
- assert key_path.exists()
- assert len(enc.key) == 32
- def test_load_or_create_loads_existing(self, temp_dir):
- """Testet load_or_create für existierende Schlüssel."""
- key_path = temp_dir / "existing.key"
- original = TrixyEncryption()
- original.save_key(key_path)
- loaded = TrixyEncryption.load_or_create(key_path)
- assert loaded.key == original.key
- def test_load_nonexistent_raises_error(self, temp_dir):
- """Testet, dass Laden nicht existierender Datei Fehler wirft."""
- with pytest.raises(FileNotFoundError):
- TrixyEncryption.load_key(temp_dir / "nonexistent.key")
- class TestCommandMessages:
- """Tests für Befehlsnachrichten."""
- def test_command_message_base(self):
- """Testet CommandMessage-Basis."""
- msg = CommandMessage()
- assert msg.message_id is not None
- assert isinstance(msg.timestamp, datetime)
- def test_satellite_connect(self):
- """Testet SatelliteConnect."""
- msg = SatelliteConnect(
- mac_address="AA:BB:CC:DD:EE:FF",
- room="wohnzimmer",
- alias="Echo 1",
- ip_address="192.168.1.100",
- version="1.0.0"
- )
- assert msg.mac_address == "AA:BB:CC:DD:EE:FF"
- assert msg.room == "wohnzimmer"
- def test_satellite_accepted(self):
- """Testet SatelliteAccepted."""
- msg = SatelliteAccepted(
- satellite_id="sat-1",
- audio_input_port=2102,
- audio_output_port=2103
- )
- assert msg.satellite_id == "sat-1"
- def test_satellite_denied(self):
- """Testet SatelliteDenied."""
- msg = SatelliteDenied(
- reason="Not registered",
- retry_allowed=True,
- retry_after_seconds=60
- )
- assert msg.reason == "Not registered"
- assert msg.retry_allowed is True
- def test_wakeword_detected(self):
- """Testet WakewordDetected."""
- msg = WakewordDetected(
- satellite_id="sat-1",
- wakeword_model="custom",
- confidence=0.95,
- audio_level=0.7
- )
- assert msg.confidence == 0.95
- def test_wakeword_selected(self):
- """Testet WakewordSelected."""
- msg = WakewordSelected(
- conversation_id="conv-123",
- start_streaming=True
- )
- assert msg.start_streaming is True
- def test_recording_start(self):
- """Testet RecordingStart."""
- msg = RecordingStart(
- conversation_id="conv-123",
- max_duration_seconds=60.0
- )
- assert msg.max_duration_seconds == 60.0
- def test_recording_done(self):
- """Testet RecordingDone."""
- msg = RecordingDone(
- conversation_id="conv-123",
- duration_seconds=5.5,
- total_samples=88000,
- ended_by="silence"
- )
- assert msg.ended_by == "silence"
- def test_system_status(self):
- """Testet SystemStatus."""
- msg = SystemStatus(
- status="running",
- uptime_seconds=3600.0,
- memory_usage_mb=256.0,
- active_satellites=3,
- loaded_plugins=["plugin1", "plugin2"]
- )
- assert msg.active_satellites == 3
- assert len(msg.loaded_plugins) == 2
- def test_command_message_to_dict(self):
- """Testet to_dict."""
- msg = SatelliteConnect(
- mac_address="AA:BB:CC:DD:EE:FF",
- room="test"
- )
- data = msg.to_dict()
- assert "mac_address" in data
- assert "room" in data
- assert data["mac_address"] == "AA:BB:CC:DD:EE:FF"
|