// 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/terminal.dart'; import 'package:flutter_tools/src/globals_null_migrated.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; setUp(() async { logger = BufferLogger.test(); testConfig = Config.test(); testTerminal = TestTerminal(); testTerminal.usesTerminalUi = true; }); testWithoutContext('No auto-sign if Xcode project settings are not available', () async { final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeam( buildSettings: null, processManager: FakeProcessManager.empty(), logger: logger, config: testConfig, terminal: testTerminal, ); expect(signingConfigs, isNull); }); testWithoutContext('No discovery if development team specified in Xcode project', () async { final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeam( buildSettings: { 'DEVELOPMENT_TEAM': 'abc', }, 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([ const FakeCommand( command: ['which', 'security'], exitCode: 1, ), ]); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeam( buildSettings: { 'bogus': 'bogus', }, processManager: processManager, logger: logger, config: testConfig, terminal: testTerminal, ); expect(signingConfigs, isNull); }); testWithoutContext('No valid code signing certificates shows instructions', () async { final FakeProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['which', 'security'], ), const FakeCommand( command: ['which', 'openssl'], ), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], ), ]); await expectLater(() => getCodeSigningIdentityDevelopmentTeam( buildSettings: {}, processManager: processManager, logger: logger, config: testConfig, terminal: testTerminal, ), throwsToolExit(message: 'No development certificates available to code sign app for device deployment')); }); testWithoutContext('Test single identity and certificate organization works', () async { final Completer completer = Completer(); final StreamController> controller = StreamController>(); const String certificates = ''' 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)" 1 valid identities found'''; final FakeProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['which', 'security'], ), const FakeCommand( command: ['which', 'openssl'], ), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: certificates, ), const FakeCommand( command: ['security', 'find-certificate', '-c', '1111AAAA11', '-p'], stdout: 'This is a fake certificate', ), FakeCommand( command: const ['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 certifacte value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeam( buildSettings: { 'bogus': 'bogus', }, 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, {'DEVELOPMENT_TEAM': '3333CCCC33'}); }); testWithoutContext('Test single identity (Catalina format) and certificate organization works', () async { final Completer completer = Completer(); final StreamController> controller = StreamController>(); const String certificates = ''' 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "Apple Development: Profile 1 (1111AAAA11)" 1 valid identities found'''; final FakeProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['which', 'security'], ), const FakeCommand( command: ['which', 'openssl'], ), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: certificates, ), const FakeCommand( command: ['security', 'find-certificate', '-c', '1111AAAA11', '-p'], stdout: 'This is a fake certificate', ), FakeCommand( command: const ['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 certifacte value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeam( buildSettings: { 'bogus': 'bogus', }, processManager: processManager, 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(signingConfigs, {'DEVELOPMENT_TEAM': '3333CCCC33'}); }); testWithoutContext('Test multiple identity and certificate organization works', () async { final Completer completer = Completer(); final StreamController> controller = StreamController>(); mockTerminalStdInStream = Stream.value('3'); final FakeProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['which', 'security'], ), const FakeCommand( command: ['which', 'openssl'], ), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), const FakeCommand( command: ['security', 'find-certificate', '-c', '3333CCCC33', '-p'], stdout: 'This is a fake certificate', ), FakeCommand( command: const ['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 certifacte value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeam( buildSettings: { 'bogus': 'bogus', }, processManager: processManager, logger: logger, config: testConfig, terminal: testTerminal, ); expect( logger.statusText, contains('Please select a certificate for code signing [1|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(signingConfigs, {'DEVELOPMENT_TEAM': '4444DDDD44'}); expect(testConfig.getValue('ios-signing-cert'), 'iPhone Developer: Profile 3 (3333CCCC33)'); }); testWithoutContext('Test multiple identity in machine mode works', () async { testTerminal.usesTerminalUi = false; final Completer completer = Completer(); final StreamController> controller = StreamController>(); final FakeProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['which', 'security'], ), const FakeCommand( command: ['which', 'openssl'], ), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), const FakeCommand( command: ['security', 'find-certificate', '-c', '1111AAAA11', '-p'], stdout: 'This is a fake certificate', ), FakeCommand( command: const ['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 certifacte value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeam( buildSettings: { 'bogus': 'bogus', }, processManager: processManager, 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(signingConfigs, {'DEVELOPMENT_TEAM': '5555EEEE55'}); }); testWithoutContext('Test saved certificate used', () async { testConfig.setValue('ios-signing-cert', 'iPhone Developer: Profile 3 (3333CCCC33)'); final Completer completer = Completer(); final StreamController> controller = StreamController>(); final FakeProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['which', 'security'], ), const FakeCommand( command: ['which', 'openssl'], ), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), const FakeCommand( command: ['security', 'find-certificate', '-c', '3333CCCC33', '-p'], stdout: 'This is a fake certificate', ), FakeCommand( command: const ['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 certifacte value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeam( buildSettings: { 'bogus': 'bogus', }, processManager: processManager, 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(signingConfigs, {'DEVELOPMENT_TEAM': '4444DDDD44'}); }); testWithoutContext('Test invalid saved certificate shows error and prompts again', () async { testConfig.setValue('ios-signing-cert', 'iPhone Developer: Invalid Profile'); mockTerminalStdInStream = Stream.value('3'); final Completer completer = Completer(); final StreamController> controller = StreamController>(); final FakeProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['which', 'security'], ), const FakeCommand( command: ['which', 'openssl'], ), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), const FakeCommand( command: ['security', 'find-certificate', '-c', '3333CCCC33', '-p'], stdout: 'This is a fake certificate', ), FakeCommand( command: const ['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 certifacte value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeam( buildSettings: { 'bogus': 'bogus', }, processManager: processManager, 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(signingConfigs, {'DEVELOPMENT_TEAM': '4444DDDD44'}); expect(stdin, 'This is a fake certificate'); expect(testConfig.getValue('ios-signing-cert'), 'iPhone Developer: Profile 3 (3333CCCC33)'); }); testWithoutContext('find-identity failure', () async { final FakeProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['which', 'security'], ), const FakeCommand( command: ['which', 'openssl'], ), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], exitCode: 1, ), ]); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeam( buildSettings: { 'bogus': 'bogus', }, processManager: processManager, logger: logger, config: testConfig, terminal: testTerminal, ); expect(signingConfigs, isNull); }); testWithoutContext('find-certificate failure', () async { mockTerminalStdInStream = Stream.value('3'); final FakeProcessManager processManager = FakeProcessManager.list([ const FakeCommand( command: ['which', 'security'], ), const FakeCommand( command: ['which', 'openssl'], ), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), const FakeCommand( command: ['security', 'find-certificate', '-c', '3333CCCC33', '-p'], exitCode: 1, ), ]); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeam( buildSettings: { 'bogus': 'bogus', }, processManager: processManager, logger: logger, config: testConfig, terminal: testTerminal, ); expect(signingConfigs, isNull); }); }); } late Stream mockTerminalStdInStream; class TestTerminal extends AnsiTerminal { TestTerminal() : super(stdio: globals.stdio, platform: globals.platform); @override String bolden(String message) => '$message'; @override Stream get keystrokes { return mockTerminalStdInStream; } @override int get preferredStyle => 0; }