import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:dmc_app/transport/link_transport.dart'; import 'package:dmc_app/transport/udp/udp_transport.dart'; void main() { // Port 0 laesst das Betriebssystem einen freien Port waehlen, damit die // Tests nicht mit einer echten mLRS-Bruecke oder parallelen Testlaeufen // um Port 14550 konkurrieren. Future openTransport({String? host}) async { final transport = UdpTransport( getHost: () async => host, getPort: () async => 0, ); await transport.connect(); return transport; } test('connect() bindet einen lokalen Port und meldet connected', () async { final transport = await openTransport(); final states = []; transport.state.listen(states.add); expect(transport.currentState, LinkState.connected); expect(transport.boundPort, isNotNull); expect(transport.boundPort, greaterThan(0)); transport.dispose(); }); test('eingehende Pakete kommen ueber incoming an und lernen die Gegenstelle', () async { final transport = await openTransport(); final sender = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0); final received = []; transport.incoming.listen(received.add); sender.send( Uint8List.fromList([1, 2, 3]), InternetAddress.loopbackIPv4, transport.boundPort!, ); await Future.delayed(const Duration(milliseconds: 50)); expect(received, [ Uint8List.fromList([1, 2, 3]), ]); expect(transport.peerDescription, contains(sender.port.toString())); sender.close(); transport.dispose(); }); test('send() ohne bekannte Gegenstelle wirft LinkConnectException', () async { final transport = await openTransport(); expect( () => transport.send(Uint8List.fromList([1])), throwsA(isA()), ); transport.dispose(); }); test('send() schickt Bytes an die gelernte Gegenstelle', () async { final transport = await openTransport(); final peer = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0); // Erst "sprechen" lassen, damit der Transport die Gegenstelle lernt // (Doku Kommunikationsschicht Abschnitt 3: "Gegenstelle aus dem Verkehr // lernen"). peer.send( Uint8List.fromList([9]), InternetAddress.loopbackIPv4, transport.boundPort!, ); await Future.delayed(const Duration(milliseconds: 50)); final receivedByPeer = []; peer.listen((event) { if (event == RawSocketEvent.read) { final datagram = peer.receive(); if (datagram != null) receivedByPeer.add(datagram.data); } }); await transport.send(Uint8List.fromList([7, 7, 7])); await Future.delayed(const Duration(milliseconds: 50)); expect(receivedByPeer, [ Uint8List.fromList([7, 7, 7]), ]); peer.close(); transport.dispose(); }); test('disconnect() setzt LinkState.disconnected', () async { final transport = await openTransport(); await transport.disconnect(); expect(transport.currentState, LinkState.disconnected); expect(transport.boundPort, isNull); transport.dispose(); }); test('weicht auf einen fluechtigen Port aus, wenn der konfigurierte belegt ist', () async { final occupied = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0); final transport = UdpTransport( getHost: () async => null, getPort: () async => occupied.port, ); await transport.connect(); expect(transport.currentState, LinkState.connected); expect(transport.boundPort, isNot(occupied.port)); expect(transport.lastErrorMessage, contains('belegt')); occupied.close(); transport.dispose(); }); test('konfigurierte Host-Vorgabe wird als initiale Gegenstelle uebernommen', () async { final transport = UdpTransport( getHost: () async => '127.0.0.1', getPort: () async => 0, ); await transport.connect(); expect(transport.peerDescription, isNotNull); expect(transport.peerDescription, startsWith('127.0.0.1')); transport.dispose(); }); }