338 lines
10 KiB
Dart
338 lines
10 KiB
Dart
import 'dart:async';
|
|
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- und Socket-Ergebnisse rein in Dart,
|
|
/// ohne einen echten Android-Platform-Channel/nativen Socket zu brauchen.
|
|
/// Der eigentliche `DatagramSocket` lebt seit der Umstellung auf
|
|
/// socket-genaue Bindung (statt Prozessbindung) nativ in
|
|
/// MlrsNetworkPlugin.kt - dieses Fake deckt daher nur noch die
|
|
/// Orchestrierung in UdpTransport ab, nicht mehr echtes UDP-Verhalten.
|
|
class FakeMlrsNetworkController implements MlrsNetworkController {
|
|
final _eventsController = StreamController<MlrsNetworkEvent>.broadcast();
|
|
final _packetsController = StreamController<MlrsSocketPacket>.broadcast();
|
|
|
|
final List<String> requestedSsidPrefixes = [];
|
|
final List<String?> requestedPreferredSsids = [];
|
|
final List<({Uint8List data, String host, int port})> sentPackets = [];
|
|
final List<String> rememberedSsids = [];
|
|
int releaseCount = 0;
|
|
int openSocketCount = 0;
|
|
int closeSocketCount = 0;
|
|
bool? staConcurrencyResult;
|
|
|
|
int nextBoundPort = 14550;
|
|
int? nextFallbackFromPort;
|
|
|
|
@override
|
|
Stream<MlrsNetworkEvent> get events => _eventsController.stream;
|
|
|
|
@override
|
|
Stream<MlrsSocketPacket> get incomingPackets => _packetsController.stream;
|
|
|
|
void emit(MlrsNetworkEvent event) => _eventsController.add(event);
|
|
void emitPacket(MlrsSocketPacket packet) => _packetsController.add(packet);
|
|
|
|
@override
|
|
Future<void> requestNetwork(String ssidPrefix, {String? preferredSsid}) async {
|
|
requestedSsidPrefixes.add(ssidPrefix);
|
|
requestedPreferredSsids.add(preferredSsid);
|
|
}
|
|
|
|
@override
|
|
Future<void> releaseNetwork() async {
|
|
releaseCount++;
|
|
}
|
|
|
|
@override
|
|
Future<UdpSocketBindResult> openSocket(int port) async {
|
|
openSocketCount++;
|
|
return UdpSocketBindResult(
|
|
boundPort: nextBoundPort,
|
|
fallbackFromPort: nextFallbackFromPort,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> closeSocket() async {
|
|
closeSocketCount++;
|
|
}
|
|
|
|
@override
|
|
Future<void> send(Uint8List data, String host, int port) async {
|
|
sentPackets.add((data: data, host: host, port: port));
|
|
}
|
|
|
|
@override
|
|
Future<bool?> isStaConcurrencySupported() async => staConcurrencyResult;
|
|
|
|
@override
|
|
void dispose() {
|
|
_eventsController.close();
|
|
_packetsController.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-',
|
|
String? rememberedSsid,
|
|
MlrsNetworkEvent event = const MlrsNetworkAvailable(),
|
|
}) async {
|
|
final transport = UdpTransport(
|
|
getHost: () async => host,
|
|
getPort: () async => port,
|
|
getSsidPrefix: () async => ssidPrefix,
|
|
getRememberedSsid: () async => rememberedSsid,
|
|
rememberSsid: (ssid) async => fake.rememberedSsids.add(ssid),
|
|
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() opens the native socket and reports the bound port',
|
|
() async {
|
|
final fake = FakeMlrsNetworkController()..nextBoundPort = 14550;
|
|
final transport = await connectWithFake(fake);
|
|
|
|
expect(transport.currentState, LinkState.connected);
|
|
expect(transport.boundPort, 14550);
|
|
expect(fake.openSocketCount, 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-',
|
|
getRememberedSsid: () async => null,
|
|
rememberSsid: (_) async {},
|
|
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);
|
|
expect(fake.openSocketCount, 0);
|
|
|
|
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);
|
|
expect(fake.closeSocketCount, 1);
|
|
|
|
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 received = <Uint8List>[];
|
|
transport.incoming.listen(received.add);
|
|
|
|
fake.emitPacket(MlrsSocketPacket(
|
|
address: '192.168.4.100',
|
|
port: 5555,
|
|
data: Uint8List.fromList([1, 2, 3]),
|
|
));
|
|
await pump();
|
|
|
|
expect(received, [
|
|
Uint8List.fromList([1, 2, 3]),
|
|
]);
|
|
expect(transport.peerDescription, '192.168.4.100:5555');
|
|
|
|
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() forwards bytes to the learned peer via the native socket',
|
|
() async {
|
|
final fake = FakeMlrsNetworkController();
|
|
final transport = await connectWithFake(fake);
|
|
|
|
fake.emitPacket(MlrsSocketPacket(
|
|
address: '192.168.4.100',
|
|
port: 5555,
|
|
data: Uint8List.fromList([9]),
|
|
));
|
|
await pump();
|
|
|
|
await transport.send(Uint8List.fromList([7, 7, 7]));
|
|
|
|
expect(fake.sentPackets, hasLength(1));
|
|
expect(fake.sentPackets.single.data, Uint8List.fromList([7, 7, 7]));
|
|
expect(fake.sentPackets.single.host, '192.168.4.100');
|
|
expect(fake.sentPackets.single.port, 5555);
|
|
|
|
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.closeSocketCount, 1);
|
|
expect(fake.releaseCount, greaterThanOrEqualTo(1));
|
|
|
|
transport.dispose();
|
|
});
|
|
|
|
test('reports a fallback port when the configured one was occupied natively',
|
|
() async {
|
|
final fake = FakeMlrsNetworkController()
|
|
..nextBoundPort = 51000
|
|
..nextFallbackFromPort = 14550;
|
|
final transport = await connectWithFake(fake, port: 14550);
|
|
|
|
expect(transport.currentState, LinkState.connected);
|
|
expect(transport.boundPort, 51000);
|
|
expect(transport.lastErrorMessage, contains('in use'));
|
|
|
|
transport.dispose();
|
|
});
|
|
|
|
test('reports isStaConcurrencySupported() once connected', () async {
|
|
final fake = FakeMlrsNetworkController()..staConcurrencyResult = true;
|
|
final transport = await connectWithFake(fake);
|
|
|
|
expect(transport.staConcurrencySupported, isTrue);
|
|
|
|
transport.dispose();
|
|
});
|
|
|
|
test('passes the remembered SSID to requestNetwork for a silent reconnect',
|
|
() async {
|
|
final fake = FakeMlrsNetworkController();
|
|
final transport = await connectWithFake(
|
|
fake,
|
|
rememberedSsid: 'mLRS-1234',
|
|
);
|
|
|
|
expect(fake.requestedPreferredSsids, ['mLRS-1234']);
|
|
|
|
transport.dispose();
|
|
});
|
|
|
|
test('remembers the connected SSID reported by the platform', () async {
|
|
final fake = FakeMlrsNetworkController();
|
|
final transport = await connectWithFake(
|
|
fake,
|
|
event: const MlrsNetworkAvailable(ssid: 'mLRS-5678'),
|
|
);
|
|
|
|
expect(transport.connectedSsid, 'mLRS-5678');
|
|
expect(fake.rememberedSsids, ['mLRS-5678']);
|
|
|
|
transport.dispose();
|
|
});
|
|
|
|
test('connectedSsid is cleared on disconnect and network loss', () async {
|
|
final fake = FakeMlrsNetworkController();
|
|
final transport = await connectWithFake(
|
|
fake,
|
|
event: const MlrsNetworkAvailable(ssid: 'mLRS-5678'),
|
|
);
|
|
expect(transport.connectedSsid, 'mLRS-5678');
|
|
|
|
await transport.disconnect();
|
|
expect(transport.connectedSsid, isNull);
|
|
|
|
transport.dispose();
|
|
});
|
|
}
|