// 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. import 'dart:async'; import 'package:file/memory.dart'; 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_port_forwarder.dart'; import 'package:flutter_tools/src/globals.dart' as globals; 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:multicast_dns/multicast_dns.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; import '../src/common.dart'; import '../src/fakes.dart'; void main() { group('mDNS Discovery', () { final int future = DateTime.now().add(const Duration(days: 1)).millisecondsSinceEpoch; setUp(() { setNetworkInterfaceLister( ({bool? includeLoopback, bool? includeLinkLocal, InternetAddressType? type}) async => [], ); }); tearDown(() { resetNetworkInterfaceLister(); }); group('for attach', () { late MDnsClient emptyClient; setUp(() { emptyClient = FakeMDnsClient([], >{}); }); testWithoutContext('Find result in preliminary client', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'bar')], >{ 'bar': [ SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'), ], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: emptyClient, preliminaryMDnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach(); expect(result, isNotNull); }); testWithoutContext( 'Do not find result in preliminary client, but find in main client', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'bar')], >{ 'bar': [ SrvResourceRecord( 'bar', future, port: 123, weight: 1, priority: 1, target: 'appId', ), ], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, preliminaryMDnsClient: emptyClient, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach(); expect(result, isNotNull); }, ); testWithoutContext('Find multiple in preliminary client', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', future, domainName: 'bar'), PtrResourceRecord('baz', future, domainName: 'fiz'), ], >{ 'bar': [ SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'), ], 'fiz': [ SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'), ], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: emptyClient, preliminaryMDnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); expect(portDiscovery.queryForAttach, throwsToolExit()); }); testWithoutContext('Find duplicates in preliminary client', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', future, domainName: 'bar'), PtrResourceRecord('foo', future, domainName: 'bar'), ], >{ 'bar': [ SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'), ], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: emptyClient, preliminaryMDnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach(); expect(result, isNotNull); }); testWithoutContext('Find similar named in preliminary client', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', future, domainName: 'bar'), PtrResourceRecord('foo', future, domainName: 'bar (2)'), ], >{ 'bar': [ SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'), ], 'bar (2)': [ SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'), ], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: emptyClient, preliminaryMDnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); expect(portDiscovery.queryForAttach, throwsToolExit()); }); testWithoutContext('No ports available', () async { final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: emptyClient, preliminaryMDnsClient: emptyClient, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final int? port = (await portDiscovery.queryForAttach())?.port; expect(port, isNull); }); testWithoutContext( 'Prints helpful message when there is no ipv4 link local address.', () async { final BufferLogger logger = BufferLogger.test(); final MemoryFileSystem fs = MemoryFileSystem.test(); final FakeAnalytics fakeAnalytics = getInitializedFakeAnalyticsInstance( fs: fs, fakeFlutterVersion: FakeFlutterVersion(), ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: emptyClient, preliminaryMDnsClient: emptyClient, logger: logger, analytics: fakeAnalytics, ); final Uri? uri = await portDiscovery.getVMServiceUriForAttach('', FakeIOSDevice()); expect(uri, isNull); expect(logger.errorText, contains('Personal Hotspot')); expect( fakeAnalytics.sentEvents, contains(Event.appleUsageEvent(workflow: 'ios-mdns', parameter: 'no-ipv4-link-local')), ); }, ); testWithoutContext('One port available, no appId', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'bar')], >{ 'bar': [ SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'), ], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, preliminaryMDnsClient: emptyClient, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final int? port = (await portDiscovery.queryForAttach())?.port; expect(port, 123); }); testWithoutContext('One port available, no appId, with authCode', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'bar')], >{ 'bar': [ SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'), ], }, txtResponse: >{ 'bar': [TxtResourceRecord('bar', future, text: 'authCode=xyz\n')], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, preliminaryMDnsClient: emptyClient, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach(); expect(result?.port, 123); expect(result?.authCode, 'xyz/'); }); testWithoutContext('Multiple ports available, with appId', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', future, domainName: 'bar'), PtrResourceRecord('baz', future, domainName: 'fiz'), ], >{ 'bar': [ SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'), ], 'fiz': [ SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'), ], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, preliminaryMDnsClient: emptyClient, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final int? port = (await portDiscovery.queryForAttach(applicationId: 'fiz'))?.port; expect(port, 321); }); testWithoutContext('Multiple ports available per process, with appId', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', future, domainName: 'bar'), PtrResourceRecord('baz', future, domainName: 'fiz'), ], >{ 'bar': [ SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'), SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'), ], 'fiz': [ SrvResourceRecord('fiz', future, port: 4321, weight: 1, priority: 1, target: 'local'), SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'), ], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, preliminaryMDnsClient: emptyClient, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final int? port = (await portDiscovery.queryForAttach(applicationId: 'bar'))?.port; expect(port, 1234); }); testWithoutContext('Throws Exception when client throws OSError on start', () async { final MDnsClient client = FakeMDnsClient( [], >{}, osErrorOnStart: true, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, preliminaryMDnsClient: emptyClient, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); expect(() async => portDiscovery.queryForAttach(), throwsException); }); testWithoutContext('Correctly builds VM Service URI with hostVmservicePort == 0', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'bar')], >{ 'bar': [ SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'), ], }, ); final FakeIOSDevice device = FakeIOSDevice(); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, preliminaryMDnsClient: emptyClient, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final Uri? uri = await portDiscovery.getVMServiceUriForAttach( 'bar', device, hostVmservicePort: 0, ); expect(uri.toString(), 'http://127.0.0.1:123/'); }); testWithoutContext('Get wireless device IP (iPv4)', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'bar')], >{ 'bar': [ SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'), ], }, ipResponse: >{ 'appId': [ IPAddressResourceRecord( 'Device IP', 0, address: InternetAddress.tryParse('111.111.111.111')!, ), ], }, txtResponse: >{ 'bar': [TxtResourceRecord('bar', future, text: 'authCode=xyz\n')], }, ); final FakeIOSDevice device = FakeIOSDevice(); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, preliminaryMDnsClient: emptyClient, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final Uri? uri = await portDiscovery.getVMServiceUriForAttach( 'bar', device, useDeviceIPAsHost: true, ); expect(uri.toString(), 'http://111.111.111.111:1234/xyz/'); }); testWithoutContext('Get wireless device IP (iPv6)', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'bar')], >{ 'bar': [ SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'), ], }, ipResponse: >{ 'appId': [ IPAddressResourceRecord( 'Device IP', 0, address: InternetAddress.tryParse('1111:1111:1111:1111:1111:1111:1111:1111')!, ), ], }, txtResponse: >{ 'bar': [TxtResourceRecord('bar', future, text: 'authCode=xyz\n')], }, ); final FakeIOSDevice device = FakeIOSDevice(); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, preliminaryMDnsClient: emptyClient, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final Uri? uri = await portDiscovery.getVMServiceUriForAttach( 'bar', device, useDeviceIPAsHost: true, ); expect(uri.toString(), 'http://[1111:1111:1111:1111:1111:1111:1111:1111]:1234/xyz/'); }); testWithoutContext( 'Throw error if unable to find VM service with app id and device port', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', future, domainName: 'srv-foo'), PtrResourceRecord('bar', future, domainName: 'srv-bar'), PtrResourceRecord('baz', future, domainName: 'srv-boo'), ], >{ 'srv-foo': [ SrvResourceRecord( 'srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo', ), ], 'srv-bar': [ SrvResourceRecord( 'srv-bar', future, port: 123, weight: 1, priority: 1, target: 'target-bar', ), ], 'srv-baz': [ SrvResourceRecord( 'srv-baz', future, port: 123, weight: 1, priority: 1, target: 'target-baz', ), ], }, ); final FakeIOSDevice device = FakeIOSDevice(); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, preliminaryMDnsClient: emptyClient, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); expect( portDiscovery.getVMServiceUriForAttach('srv-bar', device, deviceVmservicePort: 321), throwsToolExit( message: 'Did not find a Dart VM Service advertised for srv-bar on port 321.', ), ); }, ); testWithoutContext('Throw error if unable to find VM Service with app id', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'srv-foo')], >{ 'srv-foo': [ SrvResourceRecord( 'srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo', ), ], }, ); final FakeIOSDevice device = FakeIOSDevice(); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, preliminaryMDnsClient: emptyClient, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); expect( portDiscovery.getVMServiceUriForAttach('srv-asdf', device), throwsToolExit(message: 'Did not find a Dart VM Service advertised for srv-asdf.'), ); }); }); group('for launch', () { testWithoutContext('Ensure either port or device name are provided', () async { final MDnsClient client = FakeMDnsClient( [], >{}, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); expect( () async => portDiscovery.queryForLaunch(applicationId: 'app-id'), throwsAssertionError, ); }); testWithoutContext('No ports available', () async { final MDnsClient client = FakeMDnsClient( [], >{}, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForLaunch( applicationId: 'app-id', deviceVmservicePort: 123, ); expect(result, null); }); testWithoutContext( 'Prints helpful message when there is no ipv4 link local address.', () async { final MDnsClient client = FakeMDnsClient( [], >{}, ); final BufferLogger logger = BufferLogger.test(); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: logger, analytics: const NoOpAnalytics(), ); final Uri? uri = await portDiscovery.getVMServiceUriForLaunch( '', FakeIOSDevice(), deviceVmservicePort: 0, ); expect(uri, isNull); expect(logger.errorText, contains('Personal Hotspot')); }, ); testWithoutContext('Throws Exception when client throws OSError on start', () async { final MDnsClient client = FakeMDnsClient( [], >{}, osErrorOnStart: true, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); expect( () async => portDiscovery.queryForLaunch(applicationId: 'app-id', deviceVmservicePort: 123), throwsException, ); }); // On macOS, the mDNS client's socket stream creates a SocketException if // the app running the tool does not have Local Network permissions. // See: https://github.com/flutter/flutter/issues/150131 test( 'On macOS, tool exits with a helpful message when mDNS lookup throws a SocketException', () async { final MDnsClient client = FakeMDnsClient( [], >{}, socketExceptionOnLookup: true, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); expect( () async => portDiscovery.firstMatchingVmService(client), throwsToolExit( message: 'Flutter could not access the local network.\n' '\n' 'Please ensure your IDE or terminal app has permission to access ' 'devices on the local network. This allows Flutter to connect to ' 'the Dart VM.\n' '\n' 'You can grant this permission in System Settings > Privacy & ' 'Security > Local Network.\n', ), ); }, // [intended] This tool exit message only works for macOS skip: !globals.platform.isMacOS, ); // On macOS, the mDNS client's socket stream creates a SocketException if // the app running the tool does not have Local Network permissions. // See: https://github.com/flutter/flutter/issues/150131 test( 'On macOS, tool exits with a helpful message when mDNS lookup throws an uncaught SocketException', () async { final MDnsClient client = FakeMDnsClient( [], >{}, uncaughtSocketExceptionOnLookup: true, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); expect( () async => portDiscovery.firstMatchingVmService(client), throwsToolExit( message: 'Flutter could not access the local network.\n' '\n' 'Please ensure your IDE or terminal app has permission to access ' 'devices on the local network. This allows Flutter to connect to ' 'the Dart VM.\n' '\n' 'You can grant this permission in System Settings > Privacy & ' 'Security > Local Network.\n', ), ); }, // [intended] This tool exit message only works for macOS skip: !globals.platform.isMacOS, ); test( 'On macOS, tool prints a helpful message when mDNS lookup throws an uncaught SocketException', () async { final MDnsClient client = FakeMDnsClient( [], >{}, uncaughtSocketExceptionOnLookup: true, ); final BufferLogger logger = BufferLogger.test(); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: logger, analytics: const NoOpAnalytics(), ); final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService( client, throwOnMissingLocalNetworkPermissionsError: false, ); expect(result, isNull); expect( logger.errorText, contains( 'Flutter could not access the local network.\n' '\n' 'Please ensure your IDE or terminal app has permission to access ' 'devices on the local network. This allows Flutter to connect to ' 'the Dart VM.\n' '\n' 'You can grant this permission in System Settings > Privacy & ' 'Security > Local Network.\n', ), ); }, // [intended] This tool exit message only works for macOS skip: !globals.platform.isMacOS, ); testWithoutContext('Correctly builds VM Service URI with hostVmservicePort == 0', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'bar')], >{ 'bar': [ SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'), ], }, ); final FakeIOSDevice device = FakeIOSDevice(); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final Uri? uri = await portDiscovery.getVMServiceUriForLaunch( 'bar', device, hostVmservicePort: 0, deviceVmservicePort: 123, ); expect(uri.toString(), 'http://127.0.0.1:123/'); }); testWithoutContext('Get wireless device IP (iPv4)', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'bar')], >{ 'bar': [ SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'), ], }, ipResponse: >{ 'appId': [ IPAddressResourceRecord( 'Device IP', 0, address: InternetAddress.tryParse('111.111.111.111')!, ), ], }, txtResponse: >{ 'bar': [TxtResourceRecord('bar', future, text: 'authCode=xyz\n')], }, ); final FakeIOSDevice device = FakeIOSDevice(); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final Uri? uri = await portDiscovery.getVMServiceUriForLaunch( 'bar', device, useDeviceIPAsHost: true, deviceVmservicePort: 1234, ); expect(uri.toString(), 'http://111.111.111.111:1234/xyz/'); }); testWithoutContext('Get wireless device IP (iPv6)', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'bar')], >{ 'bar': [ SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'), ], }, ipResponse: >{ 'appId': [ IPAddressResourceRecord( 'Device IP', 0, address: InternetAddress.tryParse('1111:1111:1111:1111:1111:1111:1111:1111')!, ), ], }, txtResponse: >{ 'bar': [TxtResourceRecord('bar', future, text: 'authCode=xyz\n')], }, ); final FakeIOSDevice device = FakeIOSDevice(); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final Uri? uri = await portDiscovery.getVMServiceUriForLaunch( 'bar', device, useDeviceIPAsHost: true, deviceVmservicePort: 1234, ); expect(uri.toString(), 'http://[1111:1111:1111:1111:1111:1111:1111:1111]:1234/xyz/'); }); testWithoutContext( 'Throw error if unable to find VM Service with app id and device port', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', future, domainName: 'srv-foo'), PtrResourceRecord('bar', future, domainName: 'srv-bar'), PtrResourceRecord('baz', future, domainName: 'srv-boo'), ], >{ 'srv-foo': [ SrvResourceRecord( 'srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo', ), ], 'srv-bar': [ SrvResourceRecord( 'srv-bar', future, port: 123, weight: 1, priority: 1, target: 'target-bar', ), ], 'srv-baz': [ SrvResourceRecord( 'srv-baz', future, port: 123, weight: 1, priority: 1, target: 'target-baz', ), ], }, ); final FakeIOSDevice device = FakeIOSDevice(); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); expect( portDiscovery.getVMServiceUriForLaunch('srv-bar', device, deviceVmservicePort: 321), throwsToolExit( message: 'Did not find a Dart VM Service advertised for srv-bar on port 321.', ), ); }, ); testWithoutContext('Matches on application id and device name', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', future, domainName: 'srv-foo'), PtrResourceRecord('bar', future, domainName: 'srv-bar'), PtrResourceRecord('baz', future, domainName: 'srv-boo'), ], >{ 'srv-bar': [ SrvResourceRecord( 'srv-foo', future, port: 123, weight: 1, priority: 1, target: 'My-Phone.local', ), ], }, ); final FakeIOSDevice device = FakeIOSDevice(name: 'My Phone'); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final Uri? uri = await portDiscovery.getVMServiceUriForLaunch('srv-bar', device); expect(uri.toString(), 'http://127.0.0.1:123/'); }); testWithoutContext( 'Throw error if unable to find VM Service with app id and device name', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', future, domainName: 'srv-foo'), PtrResourceRecord('bar', future, domainName: 'srv-bar'), PtrResourceRecord('baz', future, domainName: 'srv-boo'), ], >{ 'srv-foo': [ SrvResourceRecord( 'srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo', ), ], }, ); final FakeIOSDevice device = FakeIOSDevice(name: 'My Phone'); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); expect( portDiscovery.getVMServiceUriForLaunch('srv-bar', device), throwsToolExit(message: 'Did not find a Dart VM Service advertised for srv-bar'), ); }, ); }); group('deviceNameMatchesTargetName', () { testWithoutContext('compares case insensitive and without spaces, hyphens, .local', () { final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: FakeMDnsClient([], >{}), logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); expect(portDiscovery.deviceNameMatchesTargetName('My phone', 'My-Phone.local'), isTrue); }); testWithoutContext('includes numbers in comparison', () { final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: FakeMDnsClient([], >{}), logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); expect(portDiscovery.deviceNameMatchesTargetName('My phone', 'My-Phone-2.local'), isFalse); }); }); testWithoutContext( 'Find firstMatchingVmService with many available and no application id', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', future, domainName: 'srv-foo'), PtrResourceRecord('bar', future, domainName: 'srv-bar'), PtrResourceRecord('baz', future, domainName: 'srv-boo'), ], >{ 'srv-foo': [ SrvResourceRecord( 'srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo', ), ], 'srv-bar': [ SrvResourceRecord( 'srv-bar', future, port: 123, weight: 1, priority: 1, target: 'target-bar', ), ], 'srv-baz': [ SrvResourceRecord( 'srv-baz', future, port: 123, weight: 1, priority: 1, target: 'target-baz', ), ], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService( client, ); expect(result?.domainName, 'srv-foo'); }, ); testWithoutContext('Find firstMatchingVmService app id', () async { final MDnsClient client = FakeMDnsClient( [ PtrResourceRecord('foo', future, domainName: 'srv-foo'), PtrResourceRecord('bar', future, domainName: 'srv-bar'), PtrResourceRecord('baz', future, domainName: 'srv-boo'), ], >{ 'srv-foo': [ SrvResourceRecord( 'srv-foo', future, port: 111, weight: 1, priority: 1, target: 'target-foo', ), ], 'srv-bar': [ SrvResourceRecord( 'srv-bar', future, port: 222, weight: 1, priority: 1, target: 'target-bar', ), SrvResourceRecord( 'srv-bar', future, port: 333, weight: 1, priority: 1, target: 'target-bar-2', ), ], 'srv-baz': [ SrvResourceRecord( 'srv-baz', future, port: 444, weight: 1, priority: 1, target: 'target-baz', ), ], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService( client, applicationId: 'srv-bar', ); expect(result?.domainName, 'srv-bar'); expect(result?.port, 222); }); testWithoutContext('find with no txt record', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'srv-foo')], >{ 'srv-foo': [ SrvResourceRecord( 'srv-foo', future, port: 111, weight: 1, priority: 1, target: 'target-foo', ), ], }, ipResponse: >{ 'target-foo': [ IPAddressResourceRecord( 'target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!, ), ], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService( client, applicationId: 'srv-foo', useDeviceIPAsHost: true, ); expect(result?.domainName, 'srv-foo'); expect(result?.port, 111); expect(result?.authCode, ''); expect(result?.ipAddress?.address, '111.111.111.111'); }); testWithoutContext('find with empty txt record', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'srv-foo')], >{ 'srv-foo': [ SrvResourceRecord( 'srv-foo', future, port: 111, weight: 1, priority: 1, target: 'target-foo', ), ], }, txtResponse: >{ 'srv-foo': [TxtResourceRecord('srv-foo', future, text: '')], }, ipResponse: >{ 'target-foo': [ IPAddressResourceRecord( 'target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!, ), ], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService( client, applicationId: 'srv-foo', useDeviceIPAsHost: true, ); expect(result?.domainName, 'srv-foo'); expect(result?.port, 111); expect(result?.authCode, ''); expect(result?.ipAddress?.address, '111.111.111.111'); }); testWithoutContext('find with valid txt record', () async { final MDnsClient client = FakeMDnsClient( [PtrResourceRecord('foo', future, domainName: 'srv-foo')], >{ 'srv-foo': [ SrvResourceRecord( 'srv-foo', future, port: 111, weight: 1, priority: 1, target: 'target-foo', ), ], }, txtResponse: >{ 'srv-foo': [ TxtResourceRecord('srv-foo', future, text: 'authCode=xyz\n'), ], }, ipResponse: >{ 'target-foo': [ IPAddressResourceRecord( 'target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!, ), ], }, ); final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery( mdnsClient: client, logger: BufferLogger.test(), analytics: const NoOpAnalytics(), ); final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService( client, applicationId: 'srv-foo', useDeviceIPAsHost: true, ); expect(result?.domainName, 'srv-foo'); expect(result?.port, 111); expect(result?.authCode, 'xyz/'); expect(result?.ipAddress?.address, '111.111.111.111'); }); }); } class FakeMDnsClient extends Fake implements MDnsClient { FakeMDnsClient( this.ptrRecords, this.srvResponse, { this.txtResponse = const >{}, this.ipResponse = const >{}, this.osErrorOnStart = false, this.socketExceptionOnLookup = false, this.uncaughtSocketExceptionOnLookup = false, }); final List ptrRecords; final Map> srvResponse; final Map> txtResponse; final Map> ipResponse; final bool osErrorOnStart; final bool socketExceptionOnLookup; final bool uncaughtSocketExceptionOnLookup; @override Future start({ InternetAddress? listenAddress, NetworkInterfacesFactory? interfacesFactory, int mDnsPort = 5353, InternetAddress? mDnsAddress, Function? onError, }) async { if (osErrorOnStart) { throw const OSError('Operation not supported on socket', 102); } } @override Stream lookup( ResourceRecordQuery query, { Duration timeout = const Duration(seconds: 5), }) { if (socketExceptionOnLookup) { throw const SocketException('Socket Exception'); } if (uncaughtSocketExceptionOnLookup) { Zone.current.handleUncaughtError( const SocketException('Socket Exception'), StackTrace.current, ); } if (T == PtrResourceRecord && query.fullyQualifiedName == MDnsVmServiceDiscovery.dartVmServiceName) { 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; } if (T == IPAddressResourceRecord) { final String key = query.fullyQualifiedName; return Stream.fromIterable( ipResponse[key] ?? [], ) as Stream; } throw UnsupportedError('Unsupported query type $T'); } @override void stop() {} } class FakeIOSDevice extends Fake implements IOSDevice { FakeIOSDevice({this.name = 'iPhone'}); @override final String name; @override Future get targetPlatform async => TargetPlatform.ios; @override bool isSupported() => true; @override bool isSupportedForProject(FlutterProject flutterProject) => true; @override DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder(); }