// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // @dart = 2.8 import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/mdns_discovery.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:multicast_dns/multicast_dns.dart'; import 'package:test/fake.dart'; import '../src/common.dart'; void main() { group('mDNS Discovery', () { final int year3000 = DateTime(3000).millisecondsSinceEpoch; setUp(() { setNetworkInterfaceLister( ({ bool includeLoopback, bool includeLinkLocal, InternetAddressType type, }) async => [], ); }); tearDown(() { resetNetworkInterfaceLister(); }); testWithoutContext('No ports available', () async { final MDnsClient client = FakeMDnsClient([], >{}); final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), flutterUsage: TestUsage(), ); final int port = (await portDiscovery.query())?.port; expect(port, isNull); }); testWithoutContext('Prints helpful message when there is no ipv4 link local address.', () async { final MDnsClient client = FakeMDnsClient([], >{}); final BufferLogger logger = BufferLogger.test(); final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: logger, flutterUsage: TestUsage(), ); final Uri uri = await portDiscovery.getObservatoryUri( '', FakeIOSDevice(), ); expect(uri, isNull); expect(logger.errorText, contains('Personal Hotspot')); }); testWithoutContext('One port available, no appId', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', year3000, domainName: 'bar'), ], >{ 'bar': [ SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), ], }, ); final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), flutterUsage: TestUsage(), ); final int port = (await portDiscovery.query())?.port; expect(port, 123); }); testWithoutContext('One port available, no appId, with authCode', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', year3000, domainName: 'bar'), ], >{ 'bar': [ SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), ], }, txtResponse: >{ 'bar': [ TxtResourceRecord('bar', year3000, text: 'authCode=xyz\n'), ], }, ); final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), flutterUsage: TestUsage(), ); final MDnsObservatoryDiscoveryResult result = await portDiscovery.query(); expect(result?.port, 123); expect(result?.authCode, 'xyz/'); }); testWithoutContext('Multiple ports available, without appId', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', year3000, domainName: 'bar'), PtrResourceRecord('baz', year3000, domainName: 'fiz'), ], >{ 'bar': [ SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), ], 'fiz': [ SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), ], }, ); final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), flutterUsage: TestUsage(), ); expect(portDiscovery.query, throwsToolExit()); }); testWithoutContext('Multiple ports available, with appId', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', year3000, domainName: 'bar'), PtrResourceRecord('baz', year3000, domainName: 'fiz'), ], >{ 'bar': [ SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), ], 'fiz': [ SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), ], }, ); final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), flutterUsage: TestUsage(), ); final int port = (await portDiscovery.query(applicationId: 'fiz'))?.port; expect(port, 321); }); testWithoutContext('Multiple ports available per process, with appId', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', year3000, domainName: 'bar'), PtrResourceRecord('baz', year3000, domainName: 'fiz'), ], >{ 'bar': [ SrvResourceRecord('bar', year3000, port: 1234, weight: 1, priority: 1, target: 'appId'), SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), ], 'fiz': [ SrvResourceRecord('fiz', year3000, port: 4321, weight: 1, priority: 1, target: 'local'), SrvResourceRecord('fiz', year3000, port: 321, weight: 1, priority: 1, target: 'local'), ], }, ); final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), flutterUsage: TestUsage(), ); final int port = (await portDiscovery.query(applicationId: 'bar'))?.port; expect(port, 1234); }); testWithoutContext('Query returns null', () async { final MDnsClient client = FakeMDnsClient( [], >{}, ); final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), flutterUsage: TestUsage(), ); final int port = (await portDiscovery.query(applicationId: 'bar'))?.port; expect(port, isNull); }); testWithoutContext('Throws Exception when client throws OSError on start', () async { final MDnsClient client = FakeMDnsClient([], >{}, osErrorOnStart: true); final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), flutterUsage: TestUsage(), ); expect( () async => portDiscovery.query(), throwsA(isA()), ); }); testWithoutContext('Correctly builds Observatory URI with hostVmservicePort == 0', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', year3000, domainName: 'bar'), ], >{ 'bar': [ SrvResourceRecord('bar', year3000, port: 123, weight: 1, priority: 1, target: 'appId'), ], }, ); final FakeIOSDevice device = FakeIOSDevice(); final MDnsObservatoryDiscovery portDiscovery = MDnsObservatoryDiscovery( mdnsClient: client, logger: BufferLogger.test(), flutterUsage: TestUsage(), ); final Uri uri = await portDiscovery.getObservatoryUri('bar', device, hostVmservicePort: 0); expect(uri.toString(), 'http://127.0.0.1:123/'); }); }); } class FakeMDnsClient extends Fake implements MDnsClient { FakeMDnsClient(this.ptrRecords, this.srvResponse, { this.txtResponse = const >{}, this.osErrorOnStart = false, }); final List ptrRecords; final Map> srvResponse; final Map> txtResponse; final bool osErrorOnStart; @override Future start({ InternetAddress listenAddress, NetworkInterfacesFactory interfacesFactory, int mDnsPort = 5353, InternetAddress mDnsAddress, }) async { if (osErrorOnStart) { throw const OSError('Operation not suppoted on socket', 102); } } @override Stream lookup( ResourceRecordQuery query, { Duration timeout = const Duration(seconds: 5), }) { if (T == PtrResourceRecord && query.fullyQualifiedName == MDnsObservatoryDiscovery.dartObservatoryName) { return Stream.fromIterable(ptrRecords) as Stream; } if (T == SrvResourceRecord) { final String key = query.fullyQualifiedName; return Stream.fromIterable(srvResponse[key] ?? []) as Stream; } if (T == TxtResourceRecord) { final String key = query.fullyQualifiedName; return Stream.fromIterable(txtResponse[key] ?? []) as Stream; } throw UnsupportedError('Unsupported query type $T'); } @override void stop() {} } class FakeIOSDevice extends Fake implements IOSDevice { @override Future get targetPlatform async => TargetPlatform.ios; @override bool isSupported() => true; @override bool isSupportedForProject(FlutterProject flutterProject) => true; @override DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder(); }