From 9d3fb1f309b5bff86801eac10c7f8c8c8d68ab7c Mon Sep 17 00:00:00 2001 From: xster Date: Thu, 18 May 2017 11:26:43 -0700 Subject: [PATCH] Auto provision iOS deploy 2/3 - prompt user to choose a certificate (#10025) * first pass * improvements * extract terminal.dart * rebase * add default terminal to context * The analyzer wants the ../ imports in front of the ./ imports * review notes --- packages/flutter_tools/lib/executable.dart | 2 + .../flutter_tools/lib/src/base/logger.dart | 67 +------ .../flutter_tools/lib/src/base/terminal.dart | 126 +++++++++++++ .../src/commands/analyze_continuously.dart | 1 + .../flutter_tools/lib/src/commands/test.dart | 1 + .../lib/src/ios/code_signing.dart | 165 ++++++++++++++++++ packages/flutter_tools/lib/src/ios/mac.dart | 132 +------------- .../lib/src/resident_runner.dart | 1 + .../test/src/base/terminal_test.dart | 47 +++++ .../{mac_test.dart => code_signing_test.dart} | 85 ++++++++- 10 files changed, 429 insertions(+), 198 deletions(-) create mode 100644 packages/flutter_tools/lib/src/base/terminal.dart create mode 100644 packages/flutter_tools/lib/src/ios/code_signing.dart create mode 100644 packages/flutter_tools/test/src/base/terminal_test.dart rename packages/flutter_tools/test/src/ios/{mac_test.dart => code_signing_test.dart} (64%) diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 6ad1e7990a6..9b3c208c384 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -18,6 +18,7 @@ import 'src/base/io.dart'; import 'src/base/logger.dart'; import 'src/base/platform.dart'; import 'src/base/process.dart'; +import 'src/base/terminal.dart'; import 'src/base/utils.dart'; import 'src/cache.dart'; import 'src/commands/analyze.dart'; @@ -120,6 +121,7 @@ Future run(List args, List subCommands, { context.putIfAbsent(Platform, () => const LocalPlatform()); context.putIfAbsent(FileSystem, () => const LocalFileSystem()); context.putIfAbsent(ProcessManager, () => const LocalProcessManager()); + context.putIfAbsent(AnsiTerminal, () => new AnsiTerminal()); context.putIfAbsent(Logger, () => platform.isWindows ? new WindowsStdoutLogger() : new StdoutLogger()); context.putIfAbsent(Config, () => new Config()); diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart index 3bcdd26f4d6..08c9228bdf9 100644 --- a/packages/flutter_tools/lib/src/base/logger.dart +++ b/packages/flutter_tools/lib/src/base/logger.dart @@ -3,16 +3,14 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:convert' show ASCII, LineSplitter; +import 'dart:convert' show LineSplitter; import 'package:meta/meta.dart'; import 'io.dart'; -import 'platform.dart'; +import 'terminal.dart'; import 'utils.dart'; -final AnsiTerminal terminal = new AnsiTerminal(); - abstract class Logger { bool get isVerbose => false; @@ -254,67 +252,6 @@ enum _LogType { trace } -class AnsiTerminal { - static const String _bold = '\u001B[1m'; - static const String _reset = '\u001B[0m'; - static const String _clear = '\u001B[2J\u001B[H'; - - static const int _ENXIO = 6; - static const int _ENOTTY = 25; - static const int _ENETRESET = 102; - static const int _INVALID_HANDLE = 6; - - /// Setting the line mode can throw for some terminals (with "Operation not - /// supported on socket"), but the error can be safely ignored. - static const List _lineModeIgnorableErrors = const [ - _ENXIO, - _ENOTTY, - _ENETRESET, - _INVALID_HANDLE, - ]; - - bool supportsColor = platform.stdoutSupportsAnsi; - - String bolden(String message) { - if (!supportsColor) - return message; - final StringBuffer buffer = new StringBuffer(); - for (String line in message.split('\n')) - buffer.writeln('$_bold$line$_reset'); - final String result = buffer.toString(); - // avoid introducing a new newline to the emboldened text - return (!message.endsWith('\n') && result.endsWith('\n')) - ? result.substring(0, result.length - 1) - : result; - } - - String clearScreen() => supportsColor ? _clear : '\n\n'; - - set singleCharMode(bool value) { - // TODO(goderbauer): instead of trying to set lineMode and then catching - // [_ENOTTY] or [_INVALID_HANDLE], we should check beforehand if stdin is - // connected to a terminal or not. - // (Requires https://github.com/dart-lang/sdk/issues/29083 to be resolved.) - try { - // The order of setting lineMode and echoMode is important on Windows. - if (value) { - stdin.echoMode = false; - stdin.lineMode = false; - } else { - stdin.lineMode = true; - stdin.echoMode = true; - } - } on StdinException catch (error) { - if (!_lineModeIgnorableErrors.contains(error.osError?.errorCode)) - rethrow; - } - } - - /// Return keystrokes from the console. - /// - /// Useful when the console is in [singleCharMode]. - Stream get onCharInput => stdin.transform(ASCII.decoder); -} class _AnsiStatus extends Status { _AnsiStatus(this.message, this.expectSlowOperation, this.onFinish) { diff --git a/packages/flutter_tools/lib/src/base/terminal.dart b/packages/flutter_tools/lib/src/base/terminal.dart new file mode 100644 index 00000000000..b00ada9b611 --- /dev/null +++ b/packages/flutter_tools/lib/src/base/terminal.dart @@ -0,0 +1,126 @@ +// Copyright 2017 The Chromium 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 'dart:convert' show ASCII; + +import 'package:quiver/strings.dart'; + +import '../globals.dart'; +import 'context.dart'; +import 'io.dart'; +import 'platform.dart'; + +final AnsiTerminal _kAnsiTerminal = new AnsiTerminal(); + +AnsiTerminal get terminal { + return context == null + ? _kAnsiTerminal + : context[AnsiTerminal]; +} + +class AnsiTerminal { + static const String _bold = '\u001B[1m'; + static const String _reset = '\u001B[0m'; + static const String _clear = '\u001B[2J\u001B[H'; + + static const int _ENXIO = 6; + static const int _ENOTTY = 25; + static const int _ENETRESET = 102; + static const int _INVALID_HANDLE = 6; + + /// Setting the line mode can throw for some terminals (with "Operation not + /// supported on socket"), but the error can be safely ignored. + static const List _lineModeIgnorableErrors = const [ + _ENXIO, + _ENOTTY, + _ENETRESET, + _INVALID_HANDLE, + ]; + + bool supportsColor = platform.stdoutSupportsAnsi; + + String bolden(String message) { + if (!supportsColor) + return message; + final StringBuffer buffer = new StringBuffer(); + for (String line in message.split('\n')) + buffer.writeln('$_bold$line$_reset'); + final String result = buffer.toString(); + // avoid introducing a new newline to the emboldened text + return (!message.endsWith('\n') && result.endsWith('\n')) + ? result.substring(0, result.length - 1) + : result; + } + + String clearScreen() => supportsColor ? _clear : '\n\n'; + + set singleCharMode(bool value) { + // TODO(goderbauer): instead of trying to set lineMode and then catching + // [_ENOTTY] or [_INVALID_HANDLE], we should check beforehand if stdin is + // connected to a terminal or not. + // (Requires https://github.com/dart-lang/sdk/issues/29083 to be resolved.) + try { + // The order of setting lineMode and echoMode is important on Windows. + if (value) { + stdin.echoMode = false; + stdin.lineMode = false; + } else { + stdin.lineMode = true; + stdin.echoMode = true; + } + } on StdinException catch (error) { + if (!_lineModeIgnorableErrors.contains(error.osError?.errorCode)) + rethrow; + } + } + + Stream _broadcastStdInString; + + /// Return keystrokes from the console. + /// + /// Useful when the console is in [singleCharMode]. + Stream get onCharInput { + if (_broadcastStdInString == null) + _broadcastStdInString = stdin.transform(ASCII.decoder).asBroadcastStream(); + return _broadcastStdInString; + } + + /// Prompts the user to input a chraracter within the accepted list. + /// Reprompts if inputted character is not in the list. + /// + /// Throws a [TimeoutException] if a `timeout` is provided and its duration + /// expired without user input. Duration resets per key press. + Future promptForCharInput( + List acceptedCharacters, { + String prompt, + bool displayAcceptedCharacters: true, + Duration timeout, + }) async { + assert(acceptedCharacters != null); + assert(acceptedCharacters.isNotEmpty); + String choice; + singleCharMode = true; + while( + isEmpty(choice) + || choice.length != 1 + || !acceptedCharacters.contains(choice) + ) { + if (isNotEmpty(prompt)) { + printStatus(prompt, emphasis: true, newline: false); + if (displayAcceptedCharacters) + printStatus(' [${acceptedCharacters.join("|")}]', newline: false); + printStatus(': ', emphasis: true, newline: false); + } + Future inputFuture = onCharInput.first; + if (timeout != null) + inputFuture = inputFuture.timeout(timeout); + choice = await inputFuture; + printStatus(choice); + } + singleCharMode = false; + return choice; + } +} + diff --git a/packages/flutter_tools/lib/src/commands/analyze_continuously.dart b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart index 8978e38378d..f94c9d7d6c7 100644 --- a/packages/flutter_tools/lib/src/commands/analyze_continuously.dart +++ b/packages/flutter_tools/lib/src/commands/analyze_continuously.dart @@ -12,6 +12,7 @@ import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; import '../base/process_manager.dart'; +import '../base/terminal.dart'; import '../base/utils.dart'; import '../cache.dart'; import '../dart/sdk.dart'; diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index ad5e824295a..e3285010a7b 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -14,6 +14,7 @@ import '../base/logger.dart'; import '../base/os.dart'; import '../base/platform.dart'; import '../base/process_manager.dart'; +import '../base/terminal.dart'; import '../cache.dart'; import '../dart/package_map.dart'; import '../globals.dart'; diff --git a/packages/flutter_tools/lib/src/ios/code_signing.dart b/packages/flutter_tools/lib/src/ios/code_signing.dart new file mode 100644 index 00000000000..56773a46b24 --- /dev/null +++ b/packages/flutter_tools/lib/src/ios/code_signing.dart @@ -0,0 +1,165 @@ +// Copyright 2017 The Chromium 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 'dart:convert' show UTF8; + +import 'package:quiver/iterables.dart'; +import 'package:quiver/strings.dart'; + +import '../application_package.dart'; +import '../base/common.dart'; +import '../base/io.dart'; +import '../base/process.dart'; +import '../base/terminal.dart'; +import '../globals.dart'; + +const String noCertificatesInstruction = ''' +═══════════════════════════════════════════════════════════════════════════════════ +No valid code signing certificates were found +Please ensure that you have a valid Development Team with valid iOS Development Certificates +associated with your Apple ID by: + 1- Opening the Xcode application + 2- Go to Xcode->Preferences->Accounts + 3- Make sure that you're signed in with your Apple ID via the '+' button on the bottom left + 4- Make sure that you have development certificates available by signing up to Apple + Developer Program and/or downloading available profiles as needed. +For more information, please visit: + https://developer.apple.com/library/content/documentation/IDEs/Conceptual/AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html + +Or run on an iOS simulator without code signing +═══════════════════════════════════════════════════════════════════════════════════'''; +const String noDevelopmentTeamInstruction = ''' +═══════════════════════════════════════════════════════════════════════════════════ +Building a deployable iOS app requires a selected Development Team with a Provisioning Profile +Please ensure that a Development Team is selected by: + 1- Opening the Flutter project's Xcode target with + open ios/Runner.xcworkspace + 2- Select the 'Runner' project in the navigator then the 'Runner' target + in the project settings + 3- In the 'General' tab, make sure a 'Development Team' is selected\n +For more information, please visit: + https://flutter.io/setup/#deploy-to-ios-devices\n +Or run on an iOS simulator +═══════════════════════════════════════════════════════════════════════════════════'''; + +final RegExp _securityFindIdentityDeveloperIdentityExtractionPattern = + new RegExp(r'^\s*\d+\).+"(.+Developer.+)"$'); +final RegExp _securityFindIdentityCertificateCnExtractionPattern = new RegExp(r'.*\(([a-zA-Z0-9]+)\)'); +final RegExp _certificateOrganizationalUnitExtractionPattern = new RegExp(r'OU=([a-zA-Z0-9]+)'); + +/// Given a [BuildableIOSApp], this will try to find valid development code +/// signing identities in the user's keychain prompting a choice if multiple +/// are found. +/// +/// Will return null if none are found, if the user cancels or if the Xcode +/// project has a development team set in the project's build settings. +Future getCodeSigningIdentityDevelopmentTeam(BuildableIOSApp iosApp) async{ + if (iosApp.buildSettings == null) + return null; + + // If the user already has it set in the project build settings itself, + // continue with that. + if (isNotEmpty(iosApp.buildSettings['DEVELOPMENT_TEAM'])) { + printStatus( + 'Automatically signing iOS for device deployment using specified development ' + 'team in Xcode project: ${iosApp.buildSettings['DEVELOPMENT_TEAM']}' + ); + return null; + } + + if (isNotEmpty(iosApp.buildSettings['PROVISIONING_PROFILE'])) + return null; + + // If the user's environment is missing the tools needed to find and read + // certificates, abandon. Tools should be pre-equipped on macOS. + if (!exitsHappy(const ['which', 'security']) || !exitsHappy(const ['which', 'openssl'])) + return null; + + final List findIdentityCommand = + const ['security', 'find-identity', '-p', 'codesigning', '-v']; + final List validCodeSigningIdentities = runCheckedSync(findIdentityCommand) + .split('\n') + .map((String outputLine) { + return _securityFindIdentityDeveloperIdentityExtractionPattern + .firstMatch(outputLine) + ?.group(1); + }) + .where(isNotEmpty) + .toSet() // Unique. + .toList(); + + final String signingIdentity = await _chooseSigningIdentity(validCodeSigningIdentities); + + // If none are chosen, return null. + if (signingIdentity == null) + return null; + + printStatus('Signing iOS app for device deployment using developer identity: "$signingIdentity"'); + + final String signingCertificateId = + _securityFindIdentityCertificateCnExtractionPattern + .firstMatch(signingIdentity) + ?.group(1); + + // If `security`'s output format changes, we'd have to update the above regex. + if (signingCertificateId == null) + return null; + + final String signingCertificate = runCheckedSync( + ['security', 'find-certificate', '-c', signingCertificateId, '-p'] + ); + + final Process opensslProcess = await runCommand(const ['openssl', 'x509', '-subject']); + opensslProcess.stdin + ..write(signingCertificate) + ..close(); + + final String opensslOutput = await UTF8.decodeStream(opensslProcess.stdout); + opensslProcess.stderr.drain(); + + if (await opensslProcess.exitCode != 0) { + return null; + } + + return _certificateOrganizationalUnitExtractionPattern + .firstMatch(opensslOutput) + ?.group(1); +} + +Future _chooseSigningIdentity(List validCodeSigningIdentities) async { + // The user has no valid code signing identities. + if (validCodeSigningIdentities.isEmpty) { + printError(noCertificatesInstruction, emphasis: true); + throwToolExit('No development certificates available to code sign app for device deployment'); + } + + if (validCodeSigningIdentities.length == 1) + return validCodeSigningIdentities.first; + + if (validCodeSigningIdentities.length > 1) { + final int count = validCodeSigningIdentities.length; + printStatus( + 'Multiple valid development certificates available:', + emphasis: true, + ); + for (int i=0; i '$number').toList() + ..add('a'), + prompt: 'Please select a certificate for code signing', + displayAcceptedCharacters: true, + ); + + if (choice == 'a') + throwToolExit('Aborted. Code signing is required to build a deployable iOS app.'); + else + return validCodeSigningIdentities[int.parse(choice) - 1]; + } + + return null; +} diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 00e4e2eb6d6..f2f0c08ec21 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -3,10 +3,9 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:convert' show JSON, UTF8; +import 'dart:convert' show JSON; import 'package:meta/meta.dart'; -import 'package:quiver/strings.dart'; import '../application_package.dart'; import '../base/common.dart'; @@ -23,6 +22,7 @@ import '../flx.dart' as flx; import '../globals.dart'; import '../plugins.dart'; import '../services.dart'; +import 'code_signing.dart'; import 'xcodeproj.dart'; const int kXcodeRequiredVersionMajor = 7; @@ -287,21 +287,7 @@ Future diagnoseXcodeBuildFailure(XcodeBuildResult result) async { if (checkBuildSettings.exitCode == 0 && !checkBuildSettings.stdout?.contains(new RegExp(r'\bDEVELOPMENT_TEAM\b')) == true && !checkBuildSettings.stdout?.contains(new RegExp(r'\bPROVISIONING_PROFILE\b')) == true) { - printError(''' -═══════════════════════════════════════════════════════════════════════════════════ -Building a deployable iOS app requires a selected Development Team with a Provisioning Profile -Please ensure that a Development Team is selected by: - 1- Opening the Flutter project's Xcode target with - open ios/Runner.xcworkspace - 2- Select the 'Runner' project in the navigator then the 'Runner' target - in the project settings - 3- In the 'General' tab, make sure a 'Development Team' is selected\n -For more information, please visit: - https://flutter.io/setup/#deploy-to-ios-devices\n -Or run on an iOS simulator -═══════════════════════════════════════════════════════════════════════════════════''', - emphasis: true, - ); + printError(noDevelopmentTeamInstruction, emphasis: true); } } } @@ -362,118 +348,6 @@ bool _checkXcodeVersion() { return true; } -final RegExp _securityFindIdentityDeveloperIdentityExtractionPattern = - new RegExp(r'^\s*\d+\).+"(.+Developer.+)"$'); -final RegExp _securityFindIdentityCertificateCnExtractionPattern = new RegExp(r'.*\(([a-zA-Z0-9]+)\)'); -final RegExp _certificateOrganizationalUnitExtractionPattern = new RegExp(r'OU=([a-zA-Z0-9]+)'); - -/// Given a [BuildableIOSApp], this will try to find valid development code -/// signing identities in the user's keychain prompting a choice if multiple -/// are found. -/// -/// Will return null if none are found, if the user cancels or if the Xcode -/// project has a development team set in the project's build settings. -Future getCodeSigningIdentityDevelopmentTeam(BuildableIOSApp iosApp) async{ - if (iosApp.buildSettings == null) - return null; - - // If the user already has it set in the project build settings itself, - // continue with that. - if (isNotEmpty(iosApp.buildSettings['DEVELOPMENT_TEAM'])) { - printStatus( - 'Automatically signing iOS for device deployment using specified development ' - 'team in Xcode project: ${iosApp.buildSettings['DEVELOPMENT_TEAM']}' - ); - return null; - } - - if (isNotEmpty(iosApp.buildSettings['PROVISIONING_PROFILE'])) - return null; - - // If the user's environment is missing the tools needed to find and read - // certificates, abandon. Tools should be pre-equipped on macOS. - if (!exitsHappy(['which', 'security']) - || !exitsHappy(['which', 'openssl'])) - return null; - - final List findIdentityCommand = - ['security', 'find-identity', '-p', 'codesigning', '-v']; - final List validCodeSigningIdentities = runCheckedSync(findIdentityCommand) - .split('\n') - .map((String outputLine) { - return _securityFindIdentityDeveloperIdentityExtractionPattern.firstMatch(outputLine)?.group(1); - }) - .where((String identityCN) => isNotEmpty(identityCN)) - .toSet() // Unique. - .toList(); - - final String signingIdentity = _chooseSigningIdentity(validCodeSigningIdentities); - - // If none are chosen. - if (signingIdentity == null) - return null; - - printStatus('Signing iOS app for device deployment using developer identity: "$signingIdentity"'); - - final String signingCertificateId = - _securityFindIdentityCertificateCnExtractionPattern.firstMatch(signingIdentity)?.group(1); - - // If `security`'s output format changes, we'd have to update this - if (signingCertificateId == null) - return null; - - final String signingCertificate = runCheckedSync( - ['security', 'find-certificate', '-c', signingCertificateId, '-p'] - ); - - final Process opensslProcess = await runCommand( - ['openssl', 'x509', '-subject'] - ); - opensslProcess.stdin - ..write(signingCertificate) - ..close(); - - final String opensslOutput = await UTF8.decodeStream(opensslProcess.stdout); - opensslProcess.stderr.drain(); - - if (await opensslProcess.exitCode != 0) { - return null; - } - - return _certificateOrganizationalUnitExtractionPattern.firstMatch(opensslOutput)?.group(1); -} - -String _chooseSigningIdentity(List validCodeSigningIdentities) { - // The user has no valid code signing identities. - if (validCodeSigningIdentities.isEmpty) { - printError( - ''' -═══════════════════════════════════════════════════════════════════════════════════ -No valid code signing certificates were found -Please ensure that you have a valid Development Team with valid iOS Development Certificates -associated with your Apple ID by: - 1- Opening the Xcode application - 2- Go to Xcode->Preferences->Accounts - 3- Make sure that you're signed in with your Apple ID via the '+' button on the bottom left - 4- Make sure that you have development certificates available by signing up to Apple - Developer Program and/or downloading available profiles as needed. -For more information, please visit: - https://developer.apple.com/library/content/documentation/IDEs/Conceptual/AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html - -Or run on an iOS simulator without code signing -═══════════════════════════════════════════════════════════════════════════════════''', - emphasis: true - ); - throwToolExit('No development certificates available to code sign app for device deployment'); - } - - // TODO(xster): let the user choose one. - if (validCodeSigningIdentities.isNotEmpty) - return validCodeSigningIdentities.first; - - return null; -} - final String noCocoaPodsConsequence = ''' CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side. Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS. diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index a568d62cd21..9194e175492 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -13,6 +13,7 @@ import 'base/common.dart'; import 'base/file_system.dart'; import 'base/io.dart'; import 'base/logger.dart'; +import 'base/terminal.dart'; import 'base/utils.dart'; import 'build_info.dart'; import 'dart/dependencies.dart'; diff --git a/packages/flutter_tools/test/src/base/terminal_test.dart b/packages/flutter_tools/test/src/base/terminal_test.dart new file mode 100644 index 00000000000..85a4c01be7e --- /dev/null +++ b/packages/flutter_tools/test/src/base/terminal_test.dart @@ -0,0 +1,47 @@ +// Copyright 2017 The Chromium 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:flutter_tools/src/base/terminal.dart'; +import 'package:test/test.dart'; + +import '../context.dart'; + + +void main() { + group('character input prompt', () { + AnsiTerminal terminalUnderTest; + + setUp(() { + terminalUnderTest = new TestTerminal(); + }); + + testUsingContext('character prompt', () async { + mockStdInStream = new Stream.fromFutures(>[ + new Future.value('d'), // Not in accepted list. + new Future.value('b'), + ]).asBroadcastStream(); + final String choice = + await terminalUnderTest.promptForCharInput( + ['a', 'b', 'c'], + prompt: 'Please choose something', + ); + expect(choice, 'b'); + expect(testLogger.statusText, ''' +Please choose something [a|b|c]: d +Please choose something [a|b|c]: b +'''); + }); + }); +} + +Stream mockStdInStream; + +class TestTerminal extends AnsiTerminal { + @override + Stream get onCharInput { + return mockStdInStream; + } +} diff --git a/packages/flutter_tools/test/src/ios/mac_test.dart b/packages/flutter_tools/test/src/ios/code_signing_test.dart similarity index 64% rename from packages/flutter_tools/test/src/ios/mac_test.dart rename to packages/flutter_tools/test/src/ios/code_signing_test.dart index 5c801ad146a..437b6ba94c3 100644 --- a/packages/flutter_tools/test/src/ios/mac_test.dart +++ b/packages/flutter_tools/test/src/ios/code_signing_test.dart @@ -8,7 +8,8 @@ import 'package:mockito/mockito.dart'; import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/io.dart'; -import 'package:flutter_tools/src/ios/mac.dart'; +import 'package:flutter_tools/src/base/terminal.dart'; +import 'package:flutter_tools/src/ios/code_signing.dart'; import 'package:process/process.dart'; import 'package:test/test.dart'; @@ -18,9 +19,11 @@ void main() { group('Auto signing', () { ProcessManager mockProcessManager; BuildableIOSApp app; + AnsiTerminal testTerminal; setUp(() { mockProcessManager = new MockProcessManager(); + testTerminal = new TestTerminal(); app = new BuildableIOSApp( projectBundleId: 'test.app', buildSettings: { @@ -81,7 +84,7 @@ void main() { ProcessManager: () => mockProcessManager, }); - testUsingContext('Test extract identity and certificate organization works', () async { + testUsingContext('Test single identity and certificate organization works', () async { when(mockProcessManager.runSync(['which', 'security'])) .thenReturn(exitsHappy); when(mockProcessManager.runSync(['which', 'openssl'])) @@ -93,8 +96,7 @@ void main() { 0, // exitCode ''' 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)" -2) da4b9237bacccdf19c0760cab7aec4a8359010b0 "iPhone Developer: Profile 2 (2222BBBB22)" - 2 valid identities found''', + 1 valid identities found''', '' )); when(mockProcessManager.runSync( @@ -135,6 +137,72 @@ void main() { overrides: { ProcessManager: () => mockProcessManager, }); + + testUsingContext('Test multiple identity and certificate organization works', () async { + when(mockProcessManager.runSync(['which', 'security'])) + .thenReturn(exitsHappy); + when(mockProcessManager.runSync(['which', 'openssl'])) + .thenReturn(exitsHappy); + when(mockProcessManager.runSync( + argThat(contains('find-identity')), environment: any, workingDirectory: any, + )).thenReturn(new ProcessResult( + 1, // pid + 0, // exitCode + ''' +1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)" +2) da4b9237bacccdf19c0760cab7aec4a8359010b0 "iPhone Developer: Profile 2 (2222BBBB22)" +3) 5bf1fd927dfb8679496a2e6cf00cbe50c1c87145 "iPhone Developer: Profile 3 (3333CCCC33)" + 3 valid identities found''', + '' + )); + mockTerminalStdInStream = + new Stream.fromFuture(new Future.value('3')); + when(mockProcessManager.runSync( + ['security', 'find-certificate', '-c', '3333CCCC33', '-p'], + environment: any, + workingDirectory: any, + )).thenReturn(new ProcessResult( + 1, // pid + 0, // exitCode + 'This is a mock certificate', + '', + )); + + final MockProcess mockOpenSslProcess = new MockProcess(); + final MockStdIn mockOpenSslStdIn = new MockStdIn(); + final MockStream mockOpenSslStdErr = new MockStream(); + + when(mockProcessManager.start( + argThat(contains('openssl')), environment: any, workingDirectory: any, + )).thenReturn(new Future.value(mockOpenSslProcess)); + + when(mockOpenSslProcess.stdin).thenReturn(mockOpenSslStdIn); + when(mockOpenSslProcess.stdout).thenReturn(new Stream>.fromFuture( + new Future>.value(UTF8.encode( + 'subject= /CN=iPhone Developer: Profile 3 (3333CCCC33)/OU=4444DDDD44/O=My Team/C=US' + )) + )); + when(mockOpenSslProcess.stderr).thenReturn(mockOpenSslStdErr); + when(mockOpenSslProcess.exitCode).thenReturn(0); + + final String developmentTeam = await getCodeSigningIdentityDevelopmentTeam(app); + + expect( + testLogger.statusText, + contains('Please select a certificate for code signing [1|2|3|a]: 3') + ); + expect( + testLogger.statusText, + contains('Signing iOS app for device deployment using developer identity: "iPhone Developer: Profile 3 (3333CCCC33)"') + ); + expect(testLogger.errorText, isEmpty); + verify(mockOpenSslStdIn.write('This is a mock certificate')); + expect(developmentTeam, '4444DDDD44'); + }, + overrides: { + ProcessManager: () => mockProcessManager, + AnsiTerminal: () => testTerminal, + }); }); } @@ -156,3 +224,12 @@ class MockProcessManager extends Mock implements ProcessManager {} class MockProcess extends Mock implements Process {} class MockStream extends Mock implements Stream> {} class MockStdIn extends Mock implements IOSink {} + +Stream mockTerminalStdInStream; + +class TestTerminal extends AnsiTerminal { + @override + Stream get onCharInput { + return mockTerminalStdInStream; + } +}