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
diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index 6ad1e79..9b3c208 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -18,6 +18,7 @@
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 @@
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 3bcdd26..08c9228 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 @@
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<int> _lineModeIgnorableErrors = const <int>[
- _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<String> 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 0000000..b00ada9
--- /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<int> _lineModeIgnorableErrors = const <int>[
+ _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<String> _broadcastStdInString;
+
+ /// Return keystrokes from the console.
+ ///
+ /// Useful when the console is in [singleCharMode].
+ Stream<String> 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<String> promptForCharInput(
+ List<String> 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<String> 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 8978e38..f94c9d7 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/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 ad5e824..e328501 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/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 0000000..56773a4
--- /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<String> 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 <String>['which', 'security']) || !exitsHappy(const <String>['which', 'openssl']))
+ return null;
+
+ final List<String> findIdentityCommand =
+ const <String>['security', 'find-identity', '-p', 'codesigning', '-v'];
+ final List<String> validCodeSigningIdentities = runCheckedSync(findIdentityCommand)
+ .split('\n')
+ .map<String>((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(
+ <String>['security', 'find-certificate', '-c', signingCertificateId, '-p']
+ );
+
+ final Process opensslProcess = await runCommand(const <String>['openssl', 'x509', '-subject']);
+ opensslProcess.stdin
+ ..write(signingCertificate)
+ ..close();
+
+ final String opensslOutput = await UTF8.decodeStream(opensslProcess.stdout);
+ opensslProcess.stderr.drain<String>();
+
+ if (await opensslProcess.exitCode != 0) {
+ return null;
+ }
+
+ return _certificateOrganizationalUnitExtractionPattern
+ .firstMatch(opensslOutput)
+ ?.group(1);
+}
+
+Future<String> _chooseSigningIdentity(List<String> 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<count; i++) {
+ printStatus(' ${i+1}) ${validCodeSigningIdentities[i]}', emphasis: true);
+ }
+ printStatus(' a) Abort', emphasis: true);
+
+ final String choice = await terminal.promptForCharInput(
+ range(1, count + 1).map((num number) => '$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 00e4e2e..f2f0c08 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 '../globals.dart';
import '../plugins.dart';
import '../services.dart';
+import 'code_signing.dart';
import 'xcodeproj.dart';
const int kXcodeRequiredVersionMajor = 7;
@@ -287,21 +287,7 @@
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 @@
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<String> 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(<String>['which', 'security'])
- || !exitsHappy(<String>['which', 'openssl']))
- return null;
-
- final List<String> findIdentityCommand =
- <String>['security', 'find-identity', '-p', 'codesigning', '-v'];
- final List<String> validCodeSigningIdentities = runCheckedSync(findIdentityCommand)
- .split('\n')
- .map<String>((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(
- <String>['security', 'find-certificate', '-c', signingCertificateId, '-p']
- );
-
- final Process opensslProcess = await runCommand(
- <String>['openssl', 'x509', '-subject']
- );
- opensslProcess.stdin
- ..write(signingCertificate)
- ..close();
-
- final String opensslOutput = await UTF8.decodeStream(opensslProcess.stdout);
- opensslProcess.stderr.drain<String>();
-
- if (await opensslProcess.exitCode != 0) {
- return null;
- }
-
- return _certificateOrganizationalUnitExtractionPattern.firstMatch(opensslOutput)?.group(1);
-}
-
-String _chooseSigningIdentity(List<String> 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 a568d62..9194e17 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/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 0000000..85a4c01
--- /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<String>.fromFutures(<Future<String>>[
+ new Future<String>.value('d'), // Not in accepted list.
+ new Future<String>.value('b'),
+ ]).asBroadcastStream();
+ final String choice =
+ await terminalUnderTest.promptForCharInput(
+ <String>['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<String> mockStdInStream;
+
+class TestTerminal extends AnsiTerminal {
+ @override
+ Stream<String> 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 5c801ad..437b6ba 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: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 @@
group('Auto signing', () {
ProcessManager mockProcessManager;
BuildableIOSApp app;
+ AnsiTerminal testTerminal;
setUp(() {
mockProcessManager = new MockProcessManager();
+ testTerminal = new TestTerminal();
app = new BuildableIOSApp(
projectBundleId: 'test.app',
buildSettings: <String, String>{
@@ -81,7 +84,7 @@
ProcessManager: () => mockProcessManager,
});
- testUsingContext('Test extract identity and certificate organization works', () async {
+ testUsingContext('Test single identity and certificate organization works', () async {
when(mockProcessManager.runSync(<String>['which', 'security']))
.thenReturn(exitsHappy);
when(mockProcessManager.runSync(<String>['which', 'openssl']))
@@ -93,8 +96,7 @@
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 @@
overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
});
+
+ testUsingContext('Test multiple identity and certificate organization works', () async {
+ when(mockProcessManager.runSync(<String>['which', 'security']))
+ .thenReturn(exitsHappy);
+ when(mockProcessManager.runSync(<String>['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<String>.fromFuture(new Future<String>.value('3'));
+ when(mockProcessManager.runSync(
+ <String>['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<Process>.value(mockOpenSslProcess));
+
+ when(mockOpenSslProcess.stdin).thenReturn(mockOpenSslStdIn);
+ when(mockOpenSslProcess.stdout).thenReturn(new Stream<List<int>>.fromFuture(
+ new Future<List<int>>.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: <Type, Generator>{
+ ProcessManager: () => mockProcessManager,
+ AnsiTerminal: () => testTerminal,
+ });
});
}
@@ -156,3 +224,12 @@
class MockProcess extends Mock implements Process {}
class MockStream extends Mock implements Stream<List<int>> {}
class MockStdIn extends Mock implements IOSink {}
+
+Stream<String> mockTerminalStdInStream;
+
+class TestTerminal extends AnsiTerminal {
+ @override
+ Stream<String> get onCharInput {
+ return mockTerminalStdInStream;
+ }
+}