Files
dmc/app/test/transport/udp_transport_test.dart
T

286 lines
8.3 KiB
Dart

import 'dart:async';
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/mlrs_network_controller.dart';
import 'package:dmc_app/transport/udp/udp_transport.dart';
/// Test-Doppel fuer [MlrsNetworkController] (Doku Kommunikationsschicht v2,
/// Abschnitt 4): simuliert Netzanfrage-Ergebnisse ueber [emit], ohne einen
/// echten Android-Platform-Channel zu brauchen. Zeichnet Aufrufe auf, damit
/// Tests den Bindungsfenster-Ablauf (bind -> Socket -> unbind) pruefen
/// koennen.
class FakeMlrsNetworkController implements MlrsNetworkController {
final _eventsController = StreamController<MlrsNetworkEvent>.broadcast();
final List<String> requestedSsidPrefixes = [];
int releaseCount = 0;
int bindCount = 0;
int unbindCount = 0;
bool bindResult = true;
bool? staConcurrencyResult;
@override
Stream<MlrsNetworkEvent> get events => _eventsController.stream;
void emit(MlrsNetworkEvent event) => _eventsController.add(event);
@override
Future<void> requestNetwork(String ssidPrefix) async {
requestedSsidPrefixes.add(ssidPrefix);
}
@override
Future<void> releaseNetwork() async {
releaseCount++;
}
@override
Future<bool> bindProcessToNetwork() async {
bindCount++;
return bindResult;
}
@override
Future<void> unbindProcessFromNetwork() async {
unbindCount++;
}
@override
Future<bool?> isStaConcurrencySupported() async => staConcurrencyResult;
@override
void dispose() {
_eventsController.close();
}
}
void main() {
// Echter Timer-Tick statt nur eines Microtasks: laesst die
// async/await-Kette in UdpTransport.connect() (getSsidPrefix ->
// requestNetwork -> Listener-Aufbau) vollstaendig ablaufen, bevor der Test
// ein Netzwerk-Ereignis einspeist.
Future<void> pump() => Future<void>.delayed(const Duration(milliseconds: 10));
Future<UdpTransport> connectWithFake(
FakeMlrsNetworkController fake, {
String? host,
int port = 0,
String ssidPrefix = 'mLRS-',
MlrsNetworkEvent event = const MlrsNetworkAvailable(),
}) async {
final transport = UdpTransport(
getHost: () async => host,
getPort: () async => port,
getSsidPrefix: () async => ssidPrefix,
networkController: fake,
);
final connectFuture = transport.connect();
await pump();
fake.emit(event);
await connectFuture;
return transport;
}
test('connect() requests the network with the configured SSID prefix',
() async {
final fake = FakeMlrsNetworkController();
final transport =
await connectWithFake(fake, ssidPrefix: 'mLRS-1234');
expect(fake.requestedSsidPrefixes, ['mLRS-1234']);
transport.dispose();
});
test(
'connect() binds process to network, creates the socket, then unbinds',
() async {
final fake = FakeMlrsNetworkController();
final transport = await connectWithFake(fake);
expect(transport.currentState, LinkState.connected);
expect(transport.boundPort, isNotNull);
expect(fake.bindCount, 1);
expect(fake.unbindCount, 1);
transport.dispose();
});
test('connect() fails with networkUnavailable when the request is denied',
() async {
final fake = FakeMlrsNetworkController();
final transport = UdpTransport(
getHost: () async => null,
getPort: () async => 0,
getSsidPrefix: () async => 'mLRS-',
networkController: fake,
);
final connectFuture = transport.connect();
await pump();
fake.emit(const MlrsNetworkUnavailable());
await expectLater(
connectFuture,
throwsA(isA<LinkConnectException>()),
);
expect(transport.lastErrorReason, LinkErrorReason.networkUnavailable);
expect(transport.currentState, LinkState.error);
transport.dispose();
});
test('losing the network while connected closes the socket and reports '
'networkLost', () async {
final fake = FakeMlrsNetworkController();
final transport = await connectWithFake(fake);
fake.emit(const MlrsNetworkLost());
await pump();
expect(transport.currentState, LinkState.error);
expect(transport.lastErrorReason, LinkErrorReason.networkLost);
expect(transport.boundPort, isNull);
transport.dispose();
});
test('uses the reported gateway address as initial peer when no host is '
'configured', () async {
final fake = FakeMlrsNetworkController();
final transport = await connectWithFake(
fake,
event: const MlrsNetworkAvailable(gatewayAddress: '192.168.4.1'),
);
expect(transport.peerDescription, startsWith('192.168.4.1'));
transport.dispose();
});
test('a configured host overrides the reported gateway address', () async {
final fake = FakeMlrsNetworkController();
final transport = await connectWithFake(
fake,
host: '10.0.0.5',
event: const MlrsNetworkAvailable(gatewayAddress: '192.168.4.1'),
);
expect(transport.peerDescription, startsWith('10.0.0.5'));
transport.dispose();
});
test('incoming packets arrive via incoming and learn the peer', () async {
final fake = FakeMlrsNetworkController();
final transport = await connectWithFake(fake);
final sender = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0);
final received = <Uint8List>[];
transport.incoming.listen(received.add);
sender.send(
Uint8List.fromList([1, 2, 3]),
InternetAddress.loopbackIPv4,
transport.boundPort!,
);
await Future<void>.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() without a known peer throws LinkConnectException', () async {
final fake = FakeMlrsNetworkController();
final transport = await connectWithFake(fake);
expect(
() => transport.send(Uint8List.fromList([1])),
throwsA(isA<LinkConnectException>()),
);
transport.dispose();
});
test('send() sends bytes to the learned peer', () async {
final fake = FakeMlrsNetworkController();
final transport = await connectWithFake(fake);
final peer = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0);
// Erst "sprechen" lassen, damit der Transport die Gegenstelle lernt
// (Doku Kommunikationsschicht v2 Abschnitt 3: "Gegenstelle aus dem
// Verkehr lernen").
peer.send(
Uint8List.fromList([9]),
InternetAddress.loopbackIPv4,
transport.boundPort!,
);
await Future<void>.delayed(const Duration(milliseconds: 50));
final receivedByPeer = <Uint8List>[];
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<void>.delayed(const Duration(milliseconds: 50));
expect(receivedByPeer, [
Uint8List.fromList([7, 7, 7]),
]);
peer.close();
transport.dispose();
});
test('disconnect() closes the socket and releases the network request',
() async {
final fake = FakeMlrsNetworkController();
final transport = await connectWithFake(fake);
await transport.disconnect();
expect(transport.currentState, LinkState.disconnected);
expect(transport.boundPort, isNull);
expect(fake.releaseCount, greaterThanOrEqualTo(1));
transport.dispose();
});
test(
'falls back to an ephemeral port when the configured one is occupied',
() async {
final occupied = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
final fake = FakeMlrsNetworkController();
final transport = await connectWithFake(fake, port: occupied.port);
expect(transport.currentState, LinkState.connected);
expect(transport.boundPort, isNot(occupied.port));
expect(transport.lastErrorMessage, contains('in use'));
occupied.close();
transport.dispose();
});
test('reports isStaConcurrencySupported() once connected', () async {
final fake = FakeMlrsNetworkController()..staConcurrencyResult = true;
final transport = await connectWithFake(fake);
expect(transport.staConcurrencySupported, isTrue);
transport.dispose();
});
}