| // 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 'dart:convert'; |
| |
| import 'package:flutter_tools/src/base/config.dart'; |
| import 'package:flutter_tools/src/base/io.dart'; |
| import 'package:flutter_tools/src/base/logger.dart'; |
| import 'package:flutter_tools/src/base/platform.dart'; |
| import 'package:flutter_tools/src/base/terminal.dart'; |
| import 'package:flutter_tools/src/globals.dart' as globals; |
| import 'package:flutter_tools/src/ios/code_signing.dart'; |
| |
| import '../../src/common.dart'; |
| import '../../src/fake_process_manager.dart'; |
| |
| const String kCertificates = ''' |
| 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'''; |
| |
| void main() { |
| group('Auto signing', () { |
| late Config testConfig; |
| late AnsiTerminal testTerminal; |
| late BufferLogger logger; |
| late Platform macosPlatform; |
| |
| setUp(() async { |
| logger = BufferLogger.test(); |
| testConfig = Config.test(); |
| testTerminal = TestTerminal(); |
| testTerminal.usesTerminalUi = true; |
| macosPlatform = FakePlatform(operatingSystem: 'macos'); |
| }); |
| |
| testWithoutContext('No auto-sign if Xcode project settings are not available', () async { |
| final Map<String, String>? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( |
| buildSettings: <String, String>{}, |
| processManager: FakeProcessManager.empty(), |
| platform: macosPlatform, |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| expect(signingConfigs, isNull); |
| }); |
| |
| testWithoutContext('No discovery if development team specified in Xcode project', () async { |
| final Map<String, String>? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( |
| buildSettings: <String, String>{ |
| 'DEVELOPMENT_TEAM': 'abc', |
| }, |
| platform: macosPlatform, |
| processManager: FakeProcessManager.empty(), |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| expect(signingConfigs, isNull); |
| expect(logger.statusText, equals( |
| 'Automatically signing iOS for device deployment using specified development team in Xcode project: abc\n' |
| )); |
| }); |
| |
| testWithoutContext('No auto-sign if security or openssl not available', () async { |
| final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ |
| const FakeCommand( |
| command: <String>['which', 'security'], |
| exitCode: 1, |
| ), |
| ]); |
| |
| final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( |
| processManager: processManager, |
| platform: macosPlatform, |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| expect(developmentTeam, isNull); |
| expect(processManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('No valid code signing certificates', () async { |
| final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ |
| const FakeCommand( |
| command: <String>['which', 'security'], |
| ), |
| const FakeCommand( |
| command: <String>['which', 'openssl'], |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-identity', '-p', 'codesigning', '-v'], |
| ), |
| ]); |
| |
| final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( |
| processManager: processManager, |
| platform: macosPlatform, |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| |
| expect(developmentTeam, isNull); |
| expect(processManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('No valid code signing certificates shows instructions', () async { |
| final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ |
| const FakeCommand( |
| command: <String>['which', 'security'], |
| ), |
| const FakeCommand( |
| command: <String>['which', 'openssl'], |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-identity', '-p', 'codesigning', '-v'], |
| ), |
| ]); |
| |
| await expectLater(() => getCodeSigningIdentityDevelopmentTeamBuildSetting( |
| buildSettings: <String, String>{}, |
| platform: macosPlatform, |
| processManager: processManager, |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ), throwsToolExit(message: 'No development certificates available to code sign app for device deployment')); |
| }); |
| |
| testWithoutContext('No valid code signing certificates on non-macOS platform', () async { |
| final FakeProcessManager processManager = FakeProcessManager.empty(); |
| |
| final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( |
| processManager: processManager, |
| platform: FakePlatform(), |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| |
| expect(developmentTeam, isNull); |
| expect(processManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('Test single identity and certificate organization development team build setting', () async { |
| final Completer<void> completer = Completer<void>(); |
| final StreamController<List<int>> controller = StreamController<List<int>>(); |
| const String certificates = ''' |
| 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)" |
| 1 valid identities found'''; |
| final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ |
| const FakeCommand( |
| command: <String>['which', 'security'], |
| ), |
| const FakeCommand( |
| command: <String>['which', 'openssl'], |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-identity', '-p', 'codesigning', '-v'], |
| stdout: certificates, |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-certificate', '-c', '1111AAAA11', '-p'], |
| stdout: 'This is a fake certificate', |
| ), |
| FakeCommand( |
| command: const <String>['openssl', 'x509', '-subject'], |
| stdin: IOSink(controller.sink), |
| stdout: 'subject= /CN=iPhone Developer: Profile 1 (1111AAAA11)/OU=3333CCCC33/O=My Team/C=US', |
| completer: completer, |
| ), |
| ]); |
| |
| // Verify that certificate value is passed into openssl command. |
| String? stdin; |
| controller.stream.listen((List<int> chunk) { |
| stdin = utf8.decode(chunk); |
| completer.complete(); |
| }); |
| |
| final Map<String, String>? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( |
| buildSettings: <String, String>{ |
| 'bogus': 'bogus', |
| }, |
| platform: macosPlatform, |
| processManager: processManager, |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| |
| expect(logger.statusText, contains('iPhone Developer: Profile 1 (1111AAAA11)')); |
| expect(logger.errorText, isEmpty); |
| expect(stdin, 'This is a fake certificate'); |
| expect(signingConfigs, <String, String>{'DEVELOPMENT_TEAM': '3333CCCC33'}); |
| expect(processManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('Test single identity and certificate organization development team', () async { |
| final Completer<void> completer = Completer<void>(); |
| final StreamController<List<int>> controller = StreamController<List<int>>(); |
| const String certificates = ''' |
| 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)" |
| 1 valid identities found'''; |
| final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ |
| const FakeCommand( |
| command: <String>['which', 'security'], |
| ), |
| const FakeCommand( |
| command: <String>['which', 'openssl'], |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-identity', '-p', 'codesigning', '-v'], |
| stdout: certificates, |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-certificate', '-c', '1111AAAA11', '-p'], |
| stdout: 'This is a fake certificate', |
| ), |
| FakeCommand( |
| command: const <String>['openssl', 'x509', '-subject'], |
| stdin: IOSink(controller.sink), |
| stdout: 'subject= /CN=iPhone Developer: Profile 1 (1111AAAA11)/OU=3333CCCC33/O=My Team/C=US', |
| completer: completer, |
| ), |
| ]); |
| |
| // Verify that certificate value is passed into openssl command. |
| String? stdin; |
| controller.stream.listen((List<int> chunk) { |
| stdin = utf8.decode(chunk); |
| completer.complete(); |
| }); |
| |
| final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( |
| processManager: processManager, |
| platform: macosPlatform, |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| |
| expect(logger.statusText, contains('iPhone Developer: Profile 1 (1111AAAA11)')); |
| expect(logger.errorText, isEmpty); |
| expect(stdin, 'This is a fake certificate'); |
| expect(developmentTeam, '3333CCCC33'); |
| expect(processManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('Test single identity (Catalina format) and certificate organization works', () async { |
| final Completer<void> completer = Completer<void>(); |
| final StreamController<List<int>> controller = StreamController<List<int>>(); |
| const String certificates = ''' |
| 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "Apple Development: Profile 1 (1111AAAA11)" |
| 1 valid identities found'''; |
| final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ |
| const FakeCommand( |
| command: <String>['which', 'security'], |
| ), |
| const FakeCommand( |
| command: <String>['which', 'openssl'], |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-identity', '-p', 'codesigning', '-v'], |
| stdout: certificates, |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-certificate', '-c', '1111AAAA11', '-p'], |
| stdout: 'This is a fake certificate', |
| ), |
| FakeCommand( |
| command: const <String>['openssl', 'x509', '-subject'], |
| stdin: IOSink(controller.sink), |
| stdout: 'subject= /CN=iPhone Developer: Profile 1 (1111AAAA11)/OU=3333CCCC33/O=My Team/C=US', |
| completer: completer, |
| ), |
| ]); |
| |
| // Verify that certificate value is passed into openssl command. |
| String? stdin; |
| controller.stream.listen((List<int> chunk) { |
| stdin = utf8.decode(chunk); |
| completer.complete(); |
| }); |
| |
| final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( |
| processManager: processManager, |
| platform: macosPlatform, |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| |
| expect(logger.statusText, contains('Apple Development: Profile 1 (1111AAAA11)')); |
| expect(logger.errorText, isEmpty); |
| expect(stdin, 'This is a fake certificate'); |
| expect(developmentTeam, '3333CCCC33'); |
| expect(processManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('Test multiple identity and certificate organization works', () async { |
| final Completer<void> completer = Completer<void>(); |
| final StreamController<List<int>> controller = StreamController<List<int>>(); |
| mockTerminalStdInStream = Stream<String>.value('3'); |
| final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ |
| const FakeCommand( |
| command: <String>['which', 'security'], |
| ), |
| const FakeCommand( |
| command: <String>['which', 'openssl'], |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-identity', '-p', 'codesigning', '-v'], |
| stdout: kCertificates, |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-certificate', '-c', '3333CCCC33', '-p'], |
| stdout: 'This is a fake certificate', |
| ), |
| FakeCommand( |
| command: const <String>['openssl', 'x509', '-subject'], |
| stdin: IOSink(controller.sink), |
| stdout: 'subject= /CN=iPhone Developer: Profile 3 (3333CCCC33)/OU=4444DDDD44/O=My Team/C=US', |
| completer: completer, |
| ), |
| ]); |
| |
| // Verify that certificate value is passed into openssl command. |
| String? stdin; |
| controller.stream.listen((List<int> chunk) { |
| stdin = utf8.decode(chunk); |
| completer.complete(); |
| }); |
| |
| final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( |
| processManager: processManager, |
| platform: macosPlatform, |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| |
| expect( |
| logger.statusText, |
| contains('Please select a certificate for code signing [<bold>1</bold>|2|3|a]: 3'), |
| ); |
| expect( |
| logger.statusText, |
| contains('Signing iOS app for device deployment using developer identity: "iPhone Developer: Profile 3 (3333CCCC33)"'), |
| ); |
| expect(logger.errorText, isEmpty); |
| expect(stdin, 'This is a fake certificate'); |
| expect(developmentTeam, '4444DDDD44'); |
| expect(testConfig.getValue('ios-signing-cert'), 'iPhone Developer: Profile 3 (3333CCCC33)'); |
| expect(processManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('Test multiple identity in machine mode works', () async { |
| testTerminal.usesTerminalUi = false; |
| final Completer<void> completer = Completer<void>(); |
| final StreamController<List<int>> controller = StreamController<List<int>>(); |
| final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ |
| const FakeCommand( |
| command: <String>['which', 'security'], |
| ), |
| const FakeCommand( |
| command: <String>['which', 'openssl'], |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-identity', '-p', 'codesigning', '-v'], |
| stdout: kCertificates, |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-certificate', '-c', '1111AAAA11', '-p'], |
| stdout: 'This is a fake certificate', |
| ), |
| FakeCommand( |
| command: const <String>['openssl', 'x509', '-subject'], |
| stdin: IOSink(controller.sink), |
| stdout: 'subject= /CN=iPhone Developer: Profile 3 (1111AAAA11)/OU=5555EEEE55/O=My Team/C=US', |
| completer: completer, |
| ), |
| ]); |
| |
| // Verify that certificate value is passed into openssl command. |
| String? stdin; |
| controller.stream.listen((List<int> chunk) { |
| stdin = utf8.decode(chunk); |
| completer.complete(); |
| }); |
| |
| final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( |
| processManager: processManager, |
| platform: macosPlatform, |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| |
| expect( |
| logger.statusText, |
| contains('Signing iOS app for device deployment using developer identity: "iPhone Developer: Profile 1 (1111AAAA11)"'), |
| ); |
| expect(logger.errorText, isEmpty); |
| expect(stdin, 'This is a fake certificate'); |
| expect(developmentTeam, '5555EEEE55'); |
| expect(processManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('Test saved certificate used', () async { |
| testConfig.setValue('ios-signing-cert', 'iPhone Developer: Profile 3 (3333CCCC33)'); |
| final Completer<void> completer = Completer<void>(); |
| final StreamController<List<int>> controller = StreamController<List<int>>(); |
| final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ |
| const FakeCommand( |
| command: <String>['which', 'security'], |
| ), |
| const FakeCommand( |
| command: <String>['which', 'openssl'], |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-identity', '-p', 'codesigning', '-v'], |
| stdout: kCertificates, |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-certificate', '-c', '3333CCCC33', '-p'], |
| stdout: 'This is a fake certificate', |
| ), |
| FakeCommand( |
| command: const <String>['openssl', 'x509', '-subject'], |
| stdin: IOSink(controller.sink), |
| stdout: 'subject= /CN=iPhone Developer: Profile 3 (3333CCCC33)/OU=4444DDDD44/O=My Team/C=US', |
| completer: completer, |
| ), |
| ]); |
| |
| // Verify that certificate value is passed into openssl command. |
| String? stdin; |
| controller.stream.listen((List<int> chunk) { |
| stdin = utf8.decode(chunk); |
| completer.complete(); |
| }); |
| |
| final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( |
| processManager: processManager, |
| platform: macosPlatform, |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| |
| expect( |
| logger.statusText, |
| contains('Found saved certificate choice "iPhone Developer: Profile 3 (3333CCCC33)". To clear, use "flutter config"'), |
| ); |
| expect( |
| logger.statusText, |
| contains('Signing iOS app for device deployment using developer identity: "iPhone Developer: Profile 3 (3333CCCC33)"'), |
| ); |
| expect(logger.errorText, isEmpty); |
| expect(stdin, 'This is a fake certificate'); |
| expect(developmentTeam, '4444DDDD44'); |
| expect(processManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('Test invalid saved certificate shows error and prompts again', () async { |
| testConfig.setValue('ios-signing-cert', 'iPhone Developer: Invalid Profile'); |
| mockTerminalStdInStream = Stream<String>.value('3'); |
| final Completer<void> completer = Completer<void>(); |
| final StreamController<List<int>> controller = StreamController<List<int>>(); |
| final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ |
| const FakeCommand( |
| command: <String>['which', 'security'], |
| ), |
| const FakeCommand( |
| command: <String>['which', 'openssl'], |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-identity', '-p', 'codesigning', '-v'], |
| stdout: kCertificates, |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-certificate', '-c', '3333CCCC33', '-p'], |
| stdout: 'This is a fake certificate', |
| ), |
| FakeCommand( |
| command: const <String>['openssl', 'x509', '-subject'], |
| stdin: IOSink(controller.sink), |
| stdout: 'subject= /CN=iPhone Developer: Profile 3 (3333CCCC33)/OU=4444DDDD44/O=My Team/C=US', |
| completer: completer, |
| ), |
| ]); |
| |
| // Verify that certificate value is passed into openssl command. |
| String? stdin; |
| controller.stream.listen((List<int> chunk) { |
| stdin = utf8.decode(chunk); |
| completer.complete(); |
| }); |
| |
| final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( |
| processManager: processManager, |
| platform: macosPlatform, |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| |
| expect( |
| logger.errorText, |
| containsIgnoringWhitespace('Saved signing certificate "iPhone Developer: Invalid Profile" is not a valid development certificate'), |
| ); |
| expect( |
| logger.statusText, |
| contains('Certificate choice "iPhone Developer: Profile 3 (3333CCCC33)"'), |
| ); |
| expect(developmentTeam, '4444DDDD44'); |
| expect(stdin, 'This is a fake certificate'); |
| expect(testConfig.getValue('ios-signing-cert'), 'iPhone Developer: Profile 3 (3333CCCC33)'); |
| expect(processManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('find-identity failure', () async { |
| final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ |
| const FakeCommand( |
| command: <String>['which', 'security'], |
| ), |
| const FakeCommand( |
| command: <String>['which', 'openssl'], |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-identity', '-p', 'codesigning', '-v'], |
| exitCode: 1, |
| ), |
| ]); |
| |
| final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( |
| processManager: processManager, |
| platform: macosPlatform, |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| expect(developmentTeam, isNull); |
| expect(processManager, hasNoRemainingExpectations); |
| }); |
| |
| testWithoutContext('find-certificate failure', () async { |
| mockTerminalStdInStream = Stream<String>.value('3'); |
| |
| final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ |
| const FakeCommand( |
| command: <String>['which', 'security'], |
| ), |
| const FakeCommand( |
| command: <String>['which', 'openssl'], |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-identity', '-p', 'codesigning', '-v'], |
| stdout: kCertificates, |
| ), |
| const FakeCommand( |
| command: <String>['security', 'find-certificate', '-c', '3333CCCC33', '-p'], |
| exitCode: 1, |
| ), |
| ]); |
| |
| final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( |
| processManager: processManager, |
| platform: macosPlatform, |
| logger: logger, |
| config: testConfig, |
| terminal: testTerminal, |
| ); |
| expect(developmentTeam, isNull); |
| expect(processManager, hasNoRemainingExpectations); |
| }); |
| }); |
| } |
| |
| late Stream<String> mockTerminalStdInStream; |
| |
| class TestTerminal extends AnsiTerminal { |
| TestTerminal() : super(stdio: globals.stdio, platform: globals.platform); |
| |
| @override |
| String bolden(String message) => '<bold>$message</bold>'; |
| |
| @override |
| Stream<String> get keystrokes { |
| return mockTerminalStdInStream; |
| } |
| |
| @override |
| int get preferredStyle => 0; |
| } |