neotalk/tests/fakes.py
Andros Fenollosa 907a0eeb55 Make the listen port configurable from the settings panel
Add a 'Your port' field in the settings panel: empty keeps local-network mode
with an ephemeral port, a value rebinds the TCP listener to a stable address
for dialing over a VPN. Active conversations survive the rebind. Adds
controller.set_listen_port and a rebindable transport.start(port).
2026-08-21 10:11:33 +02:00

142 lines
4.2 KiB
Python

"""Test doubles that honour the gateway contracts without real I/O."""
from typing import Any
class FakeCrypto:
"""A trivial, reversible stand-in for :class:`CryptoGateway`.
It does not encrypt anything: it simply prefixes the plaintext so tests can
assert that sealing and opening happened, without pulling in real crypto.
"""
_PREFIX = b"SEALED:"
def __init__(self, public_key: bytes = b"local-public-key") -> None:
self._public_key = public_key
def public_key_bytes(self) -> bytes:
return self._public_key
def derive_shared_key(self, peer_public_key: bytes) -> bytes:
return b"shared:" + peer_public_key
def encrypt(self, key: bytes, plaintext: bytes) -> bytes:
return self._PREFIX + plaintext
def decrypt(self, key: bytes, ciphertext: bytes) -> bytes:
if not ciphertext.startswith(self._PREFIX):
raise ValueError("not sealed by FakeCrypto")
return ciphertext[len(self._PREFIX) :]
class ExplodingCrypto:
"""Crypto double whose ``decrypt`` always fails, for error-path tests."""
def public_key_bytes(self) -> bytes:
return b"x"
def derive_shared_key(self, peer_public_key: bytes) -> bytes:
return b"k"
def encrypt(self, key: bytes, plaintext: bytes) -> bytes:
return plaintext
def decrypt(self, key: bytes, ciphertext: bytes) -> bytes:
raise ValueError("boom")
class FakeConnection:
"""In-memory :class:`ConnectionGateway` for controller tests."""
def __init__(self) -> None:
self.sent: list[dict[str, Any]] = []
self.closed = False
self._on_frame = None
self._on_close = None
def set_on_frame(self, callback) -> None:
self._on_frame = callback
def set_on_close(self, callback) -> None:
self._on_close = callback
def send(self, frame: dict[str, Any]) -> None:
self.sent.append(frame)
def close(self) -> None:
self.closed = True
if self._on_close is not None:
self._on_close()
def deliver(self, frame: dict[str, Any]) -> None:
"""Simulate the peer sending us ``frame``."""
if self._on_frame is not None:
self._on_frame(frame)
class FakeTransport:
"""In-memory :class:`TransportGateway` for controller tests."""
def __init__(self) -> None:
self.on_connection = None
self.outgoing: list[FakeConnection] = []
self.started = False
self.port = 55555
def start(self, on_connection, port: int | None = None) -> int:
self.on_connection = on_connection
self.started = True
if port:
self.port = port
return self.port
def stop(self) -> None:
self.started = False
def connect(self, host: str, port: int) -> FakeConnection:
# Outgoing connections are not routed through on_connection, matching
# the real transport where only the server side is notified.
connection = FakeConnection()
self.outgoing.append(connection)
return connection
def accept(self, connection: FakeConnection) -> None:
"""Simulate an incoming connection from a peer."""
self.on_connection(connection)
class FakeNotifier:
def __init__(self) -> None:
self.notifications: list[tuple[str, str]] = []
def notify(self, title: str, body: str) -> None:
self.notifications.append((title, body))
class FixedClock:
def __init__(self, value: float = 0.0) -> None:
self.value = value
def now(self) -> float:
return self.value
class RecordingDiscovery:
"""Records datagrams instead of sending them over the network."""
def __init__(self) -> None:
self.broadcasts: list[dict[str, Any]] = []
self.directed: list[tuple[str, dict[str, Any]]] = []
def start(self, on_datagram) -> None: # pragma: no cover - trivial
self._on_datagram = on_datagram
def stop(self) -> None: # pragma: no cover - trivial
pass
def broadcast(self, datagram: dict[str, Any]) -> None:
self.broadcasts.append(datagram)
def send_to(self, host: str, datagram: dict[str, Any]) -> None:
self.directed.append((host, datagram))