Commit 9d3fb1f3 authored by xster's avatar xster Committed by GitHub

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
parent bac2f0d0
...@@ -18,6 +18,7 @@ import 'src/base/io.dart'; ...@@ -18,6 +18,7 @@ import 'src/base/io.dart';
import 'src/base/logger.dart'; import 'src/base/logger.dart';
import 'src/base/platform.dart'; import 'src/base/platform.dart';
import 'src/base/process.dart'; import 'src/base/process.dart';
import 'src/base/terminal.dart';
import 'src/base/utils.dart'; import 'src/base/utils.dart';
import 'src/cache.dart'; import 'src/cache.dart';
import 'src/commands/analyze.dart'; import 'src/commands/analyze.dart';
...@@ -120,6 +121,7 @@ Future<int> run(List<String> args, List<FlutterCommand> subCommands, { ...@@ -120,6 +121,7 @@ Future<int> run(List<String> args, List<FlutterCommand> subCommands, {
context.putIfAbsent(Platform, () => const LocalPlatform()); context.putIfAbsent(Platform, () => const LocalPlatform());
context.putIfAbsent(FileSystem, () => const LocalFileSystem()); context.putIfAbsent(FileSystem, () => const LocalFileSystem());
context.putIfAbsent(ProcessManager, () => const LocalProcessManager()); context.putIfAbsent(ProcessManager, () => const LocalProcessManager());
context.putIfAbsent(AnsiTerminal, () => new AnsiTerminal());
context.putIfAbsent(Logger, () => platform.isWindows ? new WindowsStdoutLogger() : new StdoutLogger()); context.putIfAbsent(Logger, () => platform.isWindows ? new WindowsStdoutLogger() : new StdoutLogger());
context.putIfAbsent(Config, () => new Config()); context.putIfAbsent(Config, () => new Config());
......
...@@ -3,16 +3,14 @@ ...@@ -3,16 +3,14 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert' show ASCII, LineSplitter; import 'dart:convert' show LineSplitter;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'io.dart'; import 'io.dart';
import 'platform.dart'; import 'terminal.dart';
import 'utils.dart'; import 'utils.dart';
final AnsiTerminal terminal = new AnsiTerminal();
abstract class Logger { abstract class Logger {
bool get isVerbose => false; bool get isVerbose => false;
...@@ -254,67 +252,6 @@ enum _LogType { ...@@ -254,67 +252,6 @@ enum _LogType {
trace 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 { class _AnsiStatus extends Status {
_AnsiStatus(this.message, this.expectSlowOperation, this.onFinish) { _AnsiStatus(this.message, this.expectSlowOperation, this.onFinish) {
......
// 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;
}
}
...@@ -12,6 +12,7 @@ import '../base/file_system.dart'; ...@@ -12,6 +12,7 @@ import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/process_manager.dart'; import '../base/process_manager.dart';
import '../base/terminal.dart';
import '../base/utils.dart'; import '../base/utils.dart';
import '../cache.dart'; import '../cache.dart';
import '../dart/sdk.dart'; import '../dart/sdk.dart';
......
...@@ -14,6 +14,7 @@ import '../base/logger.dart'; ...@@ -14,6 +14,7 @@ import '../base/logger.dart';
import '../base/os.dart'; import '../base/os.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../base/process_manager.dart'; import '../base/process_manager.dart';
import '../base/terminal.dart';
import '../cache.dart'; import '../cache.dart';
import '../dart/package_map.dart'; import '../dart/package_map.dart';
import '../globals.dart'; import '../globals.dart';
......
// 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;
}
...@@ -3,10 +3,9 @@ ...@@ -3,10 +3,9 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert' show JSON, UTF8; import 'dart:convert' show JSON;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:quiver/strings.dart';
import '../application_package.dart'; import '../application_package.dart';
import '../base/common.dart'; import '../base/common.dart';
...@@ -23,6 +22,7 @@ import '../flx.dart' as flx; ...@@ -23,6 +22,7 @@ import '../flx.dart' as flx;
import '../globals.dart'; import '../globals.dart';
import '../plugins.dart'; import '../plugins.dart';
import '../services.dart'; import '../services.dart';
import 'code_signing.dart';
import 'xcodeproj.dart'; import 'xcodeproj.dart';
const int kXcodeRequiredVersionMajor = 7; const int kXcodeRequiredVersionMajor = 7;
...@@ -287,21 +287,7 @@ Future<Null> diagnoseXcodeBuildFailure(XcodeBuildResult result) async { ...@@ -287,21 +287,7 @@ Future<Null> diagnoseXcodeBuildFailure(XcodeBuildResult result) async {
if (checkBuildSettings.exitCode == 0 && if (checkBuildSettings.exitCode == 0 &&
!checkBuildSettings.stdout?.contains(new RegExp(r'\bDEVELOPMENT_TEAM\b')) == true && !checkBuildSettings.stdout?.contains(new RegExp(r'\bDEVELOPMENT_TEAM\b')) == true &&
!checkBuildSettings.stdout?.contains(new RegExp(r'\bPROVISIONING_PROFILE\b')) == true) { !checkBuildSettings.stdout?.contains(new RegExp(r'\bPROVISIONING_PROFILE\b')) == true) {
printError(''' printError(noDevelopmentTeamInstruction, emphasis: true);
═══════════════════════════════════════════════════════════════════════════════════
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,
);
} }
} }
} }
...@@ -362,118 +348,6 @@ bool _checkXcodeVersion() { ...@@ -362,118 +348,6 @@ bool _checkXcodeVersion() {
return true; 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 = ''' 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. 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. Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS.
......
...@@ -13,6 +13,7 @@ import 'base/common.dart'; ...@@ -13,6 +13,7 @@ import 'base/common.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'base/io.dart'; import 'base/io.dart';
import 'base/logger.dart'; import 'base/logger.dart';
import 'base/terminal.dart';
import 'base/utils.dart'; import 'base/utils.dart';
import 'build_info.dart'; import 'build_info.dart';
import 'dart/dependencies.dart'; import 'dart/dependencies.dart';
......
// 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;
}
}
...@@ -8,7 +8,8 @@ import 'package:mockito/mockito.dart'; ...@@ -8,7 +8,8 @@ import 'package:mockito/mockito.dart';
import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/io.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:process/process.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -18,9 +19,11 @@ void main() { ...@@ -18,9 +19,11 @@ void main() {
group('Auto signing', () { group('Auto signing', () {
ProcessManager mockProcessManager; ProcessManager mockProcessManager;
BuildableIOSApp app; BuildableIOSApp app;
AnsiTerminal testTerminal;
setUp(() { setUp(() {
mockProcessManager = new MockProcessManager(); mockProcessManager = new MockProcessManager();
testTerminal = new TestTerminal();
app = new BuildableIOSApp( app = new BuildableIOSApp(
projectBundleId: 'test.app', projectBundleId: 'test.app',
buildSettings: <String, String>{ buildSettings: <String, String>{
...@@ -81,7 +84,7 @@ void main() { ...@@ -81,7 +84,7 @@ void main() {
ProcessManager: () => mockProcessManager, 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'])) when(mockProcessManager.runSync(<String>['which', 'security']))
.thenReturn(exitsHappy); .thenReturn(exitsHappy);
when(mockProcessManager.runSync(<String>['which', 'openssl'])) when(mockProcessManager.runSync(<String>['which', 'openssl']))
...@@ -93,8 +96,7 @@ void main() { ...@@ -93,8 +96,7 @@ void main() {
0, // exitCode 0, // exitCode
''' '''
1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)" 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)"
2) da4b9237bacccdf19c0760cab7aec4a8359010b0 "iPhone Developer: Profile 2 (2222BBBB22)" 1 valid identities found''',
2 valid identities found''',
'' ''
)); ));
when(mockProcessManager.runSync( when(mockProcessManager.runSync(
...@@ -135,6 +137,72 @@ void main() { ...@@ -135,6 +137,72 @@ void main() {
overrides: <Type, Generator>{ overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager, 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 MockProcessManager extends Mock implements ProcessManager {} ...@@ -156,3 +224,12 @@ class MockProcessManager extends Mock implements ProcessManager {}
class MockProcess extends Mock implements Process {} class MockProcess extends Mock implements Process {}
class MockStream extends Mock implements Stream<List<int>> {} class MockStream extends Mock implements Stream<List<int>> {}
class MockStdIn extends Mock implements IOSink {} class MockStdIn extends Mock implements IOSink {}
Stream<String> mockTerminalStdInStream;
class TestTerminal extends AnsiTerminal {
@override
Stream<String> get onCharInput {
return mockTerminalStdInStream;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment