import 'dart:async';
import 'dart:io';
import 'package:multicast_dns/multicast_dns.dart';
import 'package:test/fake.dart';
import 'package:test/test.dart';
void main() {
test('Can inject datagram socket factory and configure mdns port', () async {
late int lastPort;
final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket();
final MDnsClient client = MDnsClient(rawDatagramSocketFactory:
(dynamic host, int port,
{bool reuseAddress = true,
bool reusePort = true,
int ttl = 1}) async {
lastPort = port;
return datagramSocket;
});
await client.start(
mDnsPort: 1234,
interfacesFactory: (InternetAddressType type) async =>
<NetworkInterface>[]);
expect(lastPort, 1234);
});
test('Closes IPv4 sockets', () async {
final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket();
final MDnsClient client = MDnsClient(rawDatagramSocketFactory:
(dynamic host, int port,
{bool reuseAddress = true,
bool reusePort = true,
int ttl = 1}) async {
return datagramSocket;
});
await client.start(
mDnsPort: 1234,
interfacesFactory: (InternetAddressType type) async =>
<NetworkInterface>[]);
expect(datagramSocket.closed, false);
client.stop();
expect(datagramSocket.closed, true);
});
test('Closes IPv6 sockets', () async {
final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket();
datagramSocket.address = InternetAddress.anyIPv6;
final MDnsClient client = MDnsClient(rawDatagramSocketFactory:
(dynamic host, int port,
{bool reuseAddress = true,
bool reusePort = true,
int ttl = 1}) async {
return datagramSocket;
});
await client.start(
mDnsPort: 1234,
interfacesFactory: (InternetAddressType type) async =>
<NetworkInterface>[]);
expect(datagramSocket.closed, false);
client.stop();
expect(datagramSocket.closed, true);
});
test('start() is idempotent', () async {
final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket();
datagramSocket.address = InternetAddress.anyIPv4;
final MDnsClient client = MDnsClient(rawDatagramSocketFactory:
(dynamic host, int port,
{bool reuseAddress = true,
bool reusePort = true,
int ttl = 1}) async {
return datagramSocket;
});
await client.start(
interfacesFactory: (InternetAddressType type) async =>
<NetworkInterface>[]);
await client.start();
await client.lookup(ResourceRecordQuery.serverPointer('_')).toList();
});
group('Bind a single socket to ANY IPv4 and more than one when IPv6', () {
final List<Map<String, Object>> testCases = <Map<String, Object>>[
<String, Object>{
'name': 'IPv4',
'datagramSocketType': InternetAddress.anyIPv4,
'interfacePrefix': '192.168.2.'
},
<String, Object>{
'name': 'IPv6',
'datagramSocketType': InternetAddress.anyIPv6,
'interfacePrefix': '2001:0db8:85a3:0000:0000:8a2e:7335:030'
}
];
for (final Map<String, Object> testCase in testCases) {
test('Bind a single socket to ANY ${testCase["name"]}', () async {
final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket();
datagramSocket.address =
testCase['datagramSocketType']! as InternetAddress;
final List<dynamic> selectedInterfacesForSendingPackets = <dynamic>[];
final MDnsClient client = MDnsClient(rawDatagramSocketFactory:
(dynamic host, int port,
{bool reuseAddress = true,
bool reusePort = true,
int ttl = 1}) async {
selectedInterfacesForSendingPackets.add(host);
return datagramSocket;
});
const int numberOfFakeInterfaces = 10;
Future<Iterable<NetworkInterface>> fakeNetworkInterfacesFactory(
InternetAddressType type) async {
final List<NetworkInterface> fakeInterfaces = <NetworkInterface>[];
for (int i = 0; i < numberOfFakeInterfaces; i++) {
fakeInterfaces.add(FakeNetworkInterface(
'inetfake$i',
<InternetAddress>[
InternetAddress("${testCase['interfacePrefix']! as String}$i")
],
0,
));
}
return Future.value(fakeInterfaces);
}
final InternetAddress listenAddress =
testCase['datagramSocketType']! as InternetAddress;
await client.start(
listenAddress: listenAddress,
mDnsPort: 1234,
interfacesFactory: fakeNetworkInterfacesFactory);
client.stop();
if (testCase['datagramSocketType'] == InternetAddress.anyIPv4) {
expect(selectedInterfacesForSendingPackets.length, 1);
} else {
expect(selectedInterfacesForSendingPackets.length,
numberOfFakeInterfaces + 1);
}
expect(selectedInterfacesForSendingPackets[0], listenAddress.address);
});
}
});
test('Calls onError callback in case of socket error', () async {
final FakeRawDatagramSocketThatSendsError datagramSocket =
FakeRawDatagramSocketThatSendsError();
final MDnsClient client = MDnsClient(
rawDatagramSocketFactory: (
dynamic host,
int port, {
bool reuseAddress = true,
bool reusePort = true,
int ttl = 1,
}) async {
return datagramSocket;
},
);
final Completer<void> onErrorCalledCompleter = Completer<void>();
await client.start(
mDnsPort: 1234,
interfacesFactory: (InternetAddressType type) async =>
<NetworkInterface>[],
onError: (Object e) {
expect(e, 'Error');
onErrorCalledCompleter.complete();
},
);
await onErrorCalledCompleter.future.timeout(const Duration(seconds: 5));
});
}
class FakeRawDatagramSocket extends Fake implements RawDatagramSocket {
@override
InternetAddress address = InternetAddress.anyIPv4;
@override
StreamSubscription<RawSocketEvent> listen(
void Function(RawSocketEvent event)? onData,
{Function? onError,
void Function()? onDone,
bool? cancelOnError}) {
return const Stream<RawSocketEvent>.empty().listen(onData,
onError: onError, cancelOnError: cancelOnError, onDone: onDone);
}
bool closed = false;
@override
void close() {
closed = true;
}
@override
int send(List<int> buffer, InternetAddress address, int port) {
return buffer.length;
}
@override
void joinMulticast(InternetAddress group, [NetworkInterface? interface]) {
}
@override
void setRawOption(RawSocketOption option) {
}
}
class FakeRawDatagramSocketThatSendsError extends Fake
implements RawDatagramSocket {
@override
InternetAddress address = InternetAddress.anyIPv4;
@override
StreamSubscription<RawSocketEvent> listen(
void Function(RawSocketEvent event)? onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError,
}) {
return Stream<RawSocketEvent>.error('Error').listen(
onData,
onError: onError,
cancelOnError: cancelOnError,
onDone: onDone,
);
}
}
class FakeNetworkInterface implements NetworkInterface {
FakeNetworkInterface(this._name, this._addresses, this._index);
final String _name;
final List<InternetAddress> _addresses;
final int _index;
@override
List<InternetAddress> get addresses => _addresses;
@override
String get name => _name;
@override
int get index => _index;
}