// 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:file/memory.dart'; import 'package:flutter_tools/src/base/config.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/ios/code_signing.dart'; import 'package:flutter_tools/src/ios/plist_parser.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; import '../../src/fake_process_manager.dart'; import '../../src/fakes.dart'; const 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', () { group('with getCodeSigningIdentityDevelopmentTeamBuildSetting', () { testWithoutContext('No auto-sign if Xcode project settings are not available', () async { final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: FakeProcessManager.empty(), platform: FakePlatform(operatingSystem: 'macos'), logger: BufferLogger.test(), config: Config.test(), terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(signingConfigs, isNull); }); testWithoutContext('No discovery if development team specified in Xcode project', () async { final logger = BufferLogger.test(); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {'DEVELOPMENT_TEAM': 'abc'}, platform: FakePlatform(operatingSystem: 'macos'), processManager: FakeProcessManager.empty(), logger: logger, config: Config.test(), terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(signingConfigs, isNull); expect( logger.statusText, equals( 'Automatically signing iOS for device deployment using specified development team in Xcode project: abc\n', ), ); expect(logger.errorText, isEmpty); expect(logger.traceText, isEmpty); }); testWithoutContext('No misleading log when CODE_SIGN_STYLE is Manual', () async { final logger = BufferLogger.test(); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: { 'DEVELOPMENT_TEAM': 'abc', 'CODE_SIGN_STYLE': 'Manual', }, platform: FakePlatform(operatingSystem: 'macos'), processManager: FakeProcessManager.empty(), logger: logger, config: Config.test(), terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(signingConfigs, isNull); // Should not log "Automatically signing..." when CODE_SIGN_STYLE is Manual. expect( logger.statusText, isNot( contains( 'Automatically signing iOS for device deployment using specified development team in Xcode project', ), ), ); expect(logger.errorText, isEmpty); expect(logger.traceText, isEmpty); }); testWithoutContext( 'No discovery if provisioning profile specified in Xcode project', () async { final logger = BufferLogger.test(); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {'PROVISIONING_PROFILE': 'abc'}, platform: FakePlatform(operatingSystem: 'macos'), processManager: FakeProcessManager.empty(), logger: logger, config: Config.test(), terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(signingConfigs, isNull); expect(logger.errorText, isEmpty); expect(logger.statusText, isEmpty); expect(logger.traceText, isEmpty); }, ); testWithoutContext( 'throws error with instructions when no valid code signing certificates', () async { final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], ), ]); final logger = BufferLogger.test(); await expectLater( () => getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, platform: FakePlatform(operatingSystem: 'macos'), processManager: processManager, logger: logger, config: Config.test(), terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ), throwsToolExit( message: 'No development certificates available to code sign app for device deployment', ), ); expect( logger.errorText, contains(''' ════════════════════════════════════════════════════════════════════════════════ No valid code signing certificates were found You can connect to your Apple Developer account by signing in with your Apple ID in Xcode and create an iOS Development Certificate as well as a Provisioning\u0020 Profile for your project by: 1- Open 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- Make sure a 'Development Team' is selected under Signing & Capabilities > Team.\u0020 You may need to: - Log in with your Apple ID in Xcode first - Ensure you have a valid unique Bundle ID - Register your device with your Apple Developer Account - Let Xcode automatically provision a profile for your app 4- Build or run your project again 5- Trust your newly created Development Certificate on your iOS device via Settings > General > Device Management > [your new certificate] > Trust 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 ════════════════════════════════════════════════════════════════════════════════'''), ); }, ); testWithoutContext('No auto-sign if security or openssl not available', () async { final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security'], exitCode: 1), ]); final logger = BufferLogger.test(); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: Config.test(), terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(signingConfigs, isNull); expect(processManager, hasNoRemainingExpectations); expect(logger.traceText, contains('Unable to validate code-signing tools')); expect(logger.errorText, isEmpty); }); testWithoutContext('No valid code signing certificates on non-macOS platform', () async { final processManager = FakeProcessManager.empty(); final logger = BufferLogger.test(); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: processManager, platform: FakePlatform(), logger: logger, config: Config.test(), terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(signingConfigs, isNull); expect(processManager, hasNoRemainingExpectations); expect(logger.traceText, contains('Unable to get code-sign settings on non-Mac platform')); }); testWithoutContext('uses saved provisioning profile', () async { final testConfig = Config.test(); final fileSystem = MemoryFileSystem.test(); final logger = BufferLogger.test(); const profileFilePath = '/path/to/profiles/1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision'; fileSystem.file(profileFilePath).createSync(recursive: true); testConfig.setValue('ios-signing-profile', profileFilePath); final File profilePlist = fileSystem.file( '/.tmp_rand0/provisioning_profiles/decoded_profile_1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision.plist', ); final File cert = fileSystem.file( '/.tmp_rand0/provisioning_profile_certificates/UUID1234_0.cer', ); final plistParser = FakePlistParser( parsedValues: >[ { 'Name': 'Company Development', 'ExpirationDate': '2026-02-20T16:04:31Z', 'IsXcodeManaged': false, 'DeveloperCertificates': >[ [0, 1, 2, 3], ], 'TeamIdentifier': ['ABCDE1F2DH'], 'UUID': 'UUID1234', }, ], ); const certificates = ''' 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "Apple Development: Company Development (12ABCD234E)" 1 valid identities found'''; final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: certificates, ), FakeCommand( command: [ 'security', 'cms', '-D', '-i', profileFilePath, '-o', profilePlist.path, ], onRun: (List command) => profilePlist.createSync(recursive: true), ), FakeCommand( command: ['openssl', 'x509', '-subject', '-in', cert.path, '-inform', 'DER'], stdout: 'subject= /UID=A123BC4D5E/CN=Apple Development: Company Development (12ABCD234E)/OU=ABCDE1F2DH/O=Company LLC/C=US', ), ]); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: FakeTerminal(), fileSystem: fileSystem, fileSystemUtils: FakeFileSystemUtils(), plistParser: plistParser, ); expect(processManager, hasNoRemainingExpectations); expect(cert.existsSync(), isTrue); expect(cert.readAsBytesSync(), [0, 1, 2, 3]); expect( logger.statusText, contains('Provisioning profile "Company Development" selected for iOS code signing'), ); expect(signingConfigs, { 'CODE_SIGN_STYLE': 'Manual', 'DEVELOPMENT_TEAM': 'ABCDE1F2DH', 'PROVISIONING_PROFILE_SPECIFIER': 'Company Development', }); }); testWithoutContext('does not use saved provisioning profile if does not exist', () async { final testConfig = Config.test(); final fileSystem = MemoryFileSystem.test(); final logger = BufferLogger.test(); const profileFilePath = '/path/to/profiles/1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision'; testConfig.setValue('ios-signing-profile', profileFilePath); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), ]); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: FakeTerminal(), fileSystem: fileSystem, fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(processManager, hasNoRemainingExpectations); expect(logger.errorText, contains('Unable to find saved provisioning profile')); expect(signingConfigs, isNull); }); testWithoutContext( 'does not use saved provisioning profile if security fails to decode', () async { final testConfig = Config.test(); final fileSystem = MemoryFileSystem.test(); final logger = BufferLogger.test(); const profileFilePath = '/path/to/profiles/1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision'; fileSystem.file(profileFilePath).createSync(recursive: true); testConfig.setValue('ios-signing-profile', profileFilePath); final File profilePlist = fileSystem.file( '/.tmp_rand0/provisioning_profiles/decoded_profile_1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision.plist', ); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), FakeCommand( command: [ 'security', 'cms', '-D', '-i', profileFilePath, '-o', profilePlist.path, ], exitCode: 1, ), ]); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: FakeTerminal(), fileSystem: fileSystem, fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(processManager, hasNoRemainingExpectations); expect(logger.errorText, contains('Unexpected failure from security')); expect(signingConfigs, isNull); }, ); testWithoutContext( 'does not use saved provisioning profile if security fails to create plist', () async { final testConfig = Config.test(); final fileSystem = MemoryFileSystem.test(); final logger = BufferLogger.test(); const profileFilePath = '/path/to/profiles/1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision'; fileSystem.file(profileFilePath).createSync(recursive: true); testConfig.setValue('ios-signing-profile', profileFilePath); final File profilePlist = fileSystem.file( '/.tmp_rand0/provisioning_profiles/decoded_profile_1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision.plist', ); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), FakeCommand( command: [ 'security', 'cms', '-D', '-i', profileFilePath, '-o', profilePlist.path, ], ), ]); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: FakeTerminal(), fileSystem: fileSystem, fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(processManager, hasNoRemainingExpectations); expect(logger.errorText, contains('Failed to decode')); expect(signingConfigs, isNull); }, ); testWithoutContext('does not use saved provisioning profile if fails to parse plist', () async { final testConfig = Config.test(); final fileSystem = MemoryFileSystem.test(); final logger = BufferLogger.test(); const profileFilePath = '/path/to/profiles/1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision'; fileSystem.file(profileFilePath).createSync(recursive: true); testConfig.setValue('ios-signing-profile', profileFilePath); final File profilePlist = fileSystem.file( '/.tmp_rand0/provisioning_profiles/decoded_profile_1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision.plist', ); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), FakeCommand( command: [ 'security', 'cms', '-D', '-i', profileFilePath, '-o', profilePlist.path, ], onRun: (List command) => profilePlist.createSync(recursive: true), ), ]); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: FakeTerminal(), fileSystem: fileSystem, fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(processManager, hasNoRemainingExpectations); expect(logger.errorText, contains('Failed to parse provisioning profile')); expect(signingConfigs, isNull); }); testWithoutContext( 'does not uses saved provisioning profile if openssl fails to read cert', () async { final testConfig = Config.test(); final fileSystem = MemoryFileSystem.test(); final logger = BufferLogger.test(); const profileFilePath = '/path/to/profiles/1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision'; fileSystem.file(profileFilePath).createSync(recursive: true); testConfig.setValue('ios-signing-profile', profileFilePath); final File profilePlist = fileSystem.file( '/.tmp_rand0/provisioning_profiles/decoded_profile_1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision.plist', ); final File cert = fileSystem.file( '/.tmp_rand0/provisioning_profile_certificates/UUID1234_0.cer', ); final plistParser = FakePlistParser( parsedValues: >[ { 'Name': 'Flutter Development', 'ExpirationDate': '2026-02-20T16:04:31Z', 'IsXcodeManaged': false, 'DeveloperCertificates': >[ [0, 1, 2, 3], ], 'TeamIdentifier': ['ABCDE1F2DH'], 'UUID': 'UUID1234', }, ], ); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), FakeCommand( command: [ 'security', 'cms', '-D', '-i', profileFilePath, '-o', profilePlist.path, ], onRun: (List command) => profilePlist.createSync(recursive: true), ), FakeCommand( command: ['openssl', 'x509', '-subject', '-in', cert.path, '-inform', 'DER'], exitCode: 1, ), ]); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: FakeTerminal(), fileSystem: fileSystem, fileSystemUtils: FakeFileSystemUtils(), plistParser: plistParser, ); expect(processManager, hasNoRemainingExpectations); expect(logger.errorText, contains('Unexpected failure from openssl')); expect(signingConfigs, isNull); }, ); testWithoutContext( 'does not uses saved provisioning profile if fails to parse common name from cert', () async { final testConfig = Config.test(); final fileSystem = MemoryFileSystem.test(); final logger = BufferLogger.test(); const profileFilePath = '/path/to/profiles/1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision'; fileSystem.file(profileFilePath).createSync(recursive: true); testConfig.setValue('ios-signing-profile', profileFilePath); final File profilePlist = fileSystem.file( '/.tmp_rand0/provisioning_profiles/decoded_profile_1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision.plist', ); final File cert = fileSystem.file( '/.tmp_rand0/provisioning_profile_certificates/UUID1234_0.cer', ); final plistParser = FakePlistParser( parsedValues: >[ { 'Name': 'Flutter Development', 'ExpirationDate': '2026-02-20T16:04:31Z', 'IsXcodeManaged': false, 'DeveloperCertificates': >[ [0, 1, 2, 3], ], 'TeamIdentifier': ['ABCDE1F2DH'], 'UUID': 'UUID1234', }, ], ); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), FakeCommand( command: [ 'security', 'cms', '-D', '-i', profileFilePath, '-o', profilePlist.path, ], onRun: (List command) => profilePlist.createSync(recursive: true), ), FakeCommand( command: ['openssl', 'x509', '-subject', '-in', cert.path, '-inform', 'DER'], ), ]); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: FakeTerminal(), fileSystem: fileSystem, fileSystemUtils: FakeFileSystemUtils(), plistParser: plistParser, ); expect(processManager, hasNoRemainingExpectations); expect(logger.errorText, contains('Unable to extract Common Name from certificate')); expect(signingConfigs, isNull); }, ); testWithoutContext( 'does not use saved provisioning profile if fails to find matching identity', () async { final testConfig = Config.test(); final fileSystem = MemoryFileSystem.test(); final logger = BufferLogger.test(); const profileFilePath = '/path/to/profiles/1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision'; fileSystem.file(profileFilePath).createSync(recursive: true); testConfig.setValue('ios-signing-profile', profileFilePath); final File profilePlist = fileSystem.file( '/.tmp_rand0/provisioning_profiles/decoded_profile_1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision.plist', ); final File cert = fileSystem.file( '/.tmp_rand0/provisioning_profile_certificates/UUID1234_0.cer', ); final plistParser = FakePlistParser( parsedValues: >[ { 'Name': 'Flutter Development', 'ExpirationDate': '2026-02-20T16:04:31Z', 'IsXcodeManaged': false, 'DeveloperCertificates': >[ [0, 1, 2, 3], ], 'TeamIdentifier': ['ABCDE1F2DH'], 'UUID': 'UUID1234', }, ], ); const certificates = ''' 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "Apple Development: Not a Match (12ABCD234E)" 1 valid identities found'''; final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: certificates, ), FakeCommand( command: [ 'security', 'cms', '-D', '-i', profileFilePath, '-o', profilePlist.path, ], onRun: (List command) => profilePlist.createSync(recursive: true), ), FakeCommand( command: ['openssl', 'x509', '-subject', '-in', cert.path, '-inform', 'DER'], stdout: 'subject= /UID=A123BC4D5E/CN=Apple Development: Company Development (12ABCD234E)/OU=ABCDE1F2DH/O=Company LLC/C=US', ), ]); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: FakeTerminal(), fileSystem: fileSystem, fileSystemUtils: FakeFileSystemUtils(), plistParser: plistParser, ); expect(processManager, hasNoRemainingExpectations); expect( logger.errorText, contains('Unable to find a valid certificate matching the provisioning profile'), ); expect(signingConfigs, isNull); }, ); testWithoutContext( 'Test single identity and certificate organization development team build setting', () async { final completer = Completer(); final controller = StreamController>(); const certificates = ''' 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)" 1 valid identities found'''; final 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 certificate value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final logger = BufferLogger.test(); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {'bogus': 'bogus'}, platform: FakePlatform(operatingSystem: 'macos'), processManager: processManager, logger: logger, config: Config.test(), terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); 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'}); expect(processManager, hasNoRemainingExpectations); }, ); testWithoutContext( 'Test auto-select single identity and certificate organization development team', () async { final completer = Completer(); final controller = StreamController>(); const certificates = ''' 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)" 1 valid identities found'''; final 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 certificate value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final testConfig = Config.test(); final logger = BufferLogger.test(); final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(logger.statusText, contains('iPhone Developer: Profile 1 (1111AAAA11)')); expect(logger.errorText, isEmpty); expect(stdin, 'This is a fake certificate'); expect(developmentTeam, '3333CCCC33'); expect(testConfig.getValue('ios-signing-cert'), isNull); expect(processManager, hasNoRemainingExpectations); }, ); testWithoutContext( 'Test single identity (Catalina format) and certificate organization works', () async { final completer = Completer(); final controller = StreamController>(); const certificates = ''' 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "Apple Development: Profile 1 (1111AAAA11)" 1 valid identities found'''; final 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 certificate value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final logger = BufferLogger.test(); final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: Config.test(), terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); 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 = Completer(); final controller = StreamController>(); final 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 certificate value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final logger = BufferLogger.test(); final testConfig = Config.test(); final testTerminal = FakeTerminal(); testTerminal.setPrompt(['1', '2', '3', 'q'], '3'); final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: testTerminal, fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect( logger.statusText, contains( 'Developer identity "iPhone Developer: Profile 3 (3333CCCC33)" selected for iOS code signing', ), ); 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 auto-select from multiple identity in machine mode works', () async { final completer = Completer(); final controller = StreamController>(); final 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 certificate value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final logger = BufferLogger.test(); final testConfig = Config.test(); final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: FakeTerminal(stdinHasTerminal: false), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect( logger.statusText, contains( 'Developer identity "iPhone Developer: Profile 1 (1111AAAA11)" selected for iOS code signing', ), ); expect(logger.errorText, isEmpty); expect(stdin, 'This is a fake certificate'); expect(developmentTeam, '5555EEEE55'); expect(testConfig.getValue('ios-signing-cert'), isNull); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('Test saved certificate used', () async { final testConfig = Config.test(); final logger = BufferLogger.test(); testConfig.setValue('ios-signing-cert', 'iPhone Developer: Profile 3 (3333CCCC33)'); final completer = Completer(); final controller = StreamController>(); final 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 certificate value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect( logger.statusText, contains( 'Found saved certificate choice "iPhone Developer: Profile 3 (3333CCCC33)". To clear, use "flutter config --clear-ios-signing-settings"', ), ); expect( logger.statusText, contains( 'Developer identity "iPhone Developer: Profile 3 (3333CCCC33)" selected for iOS code signing', ), ); expect(logger.errorText, isEmpty); expect(stdin, 'This is a fake certificate'); expect(signingConfigs, {'DEVELOPMENT_TEAM': '4444DDDD44'}); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('Test invalid saved certificate shows error and prompts again', () async { final testConfig = Config.test(); testConfig.setValue('ios-signing-cert', 'iPhone Developer: Invalid Profile'); final completer = Completer(); final controller = StreamController>(); final 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, ), ]); final testTerminal = FakeTerminal(); testTerminal.setPrompt(['1', '2', '3', 'q'], '3'); // Verify that certificate value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final logger = BufferLogger.test(); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: testTerminal, fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); 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)'); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('find-identity failure', () async { final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], exitCode: 1, ), ]); await expectLater( () => getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, platform: FakePlatform(operatingSystem: 'macos'), processManager: processManager, logger: BufferLogger.test(), config: Config.test(), terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ), throwsToolExit( message: 'No development certificates available to code sign app for device deployment', ), ); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('find-certificate failure', () async { final 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 testTerminal = FakeTerminal(); testTerminal.setPrompt(['1', '2', '3', 'q'], '3'); final logger = BufferLogger.test(); final Map? signingConfigs = await getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: {}, processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: Config.test(), terminal: testTerminal, fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(signingConfigs, isNull); expect(processManager, hasNoRemainingExpectations); expect(logger.errorText, contains('Unexpected error from security')); }); testWithoutContext('handles stdin pipe breaking on openssl process', () async { final StreamSink> stdinSink = ClosedStdinController(); final completer = Completer(); const certificates = ''' 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)" 1 valid identities found'''; final 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(stdinSink), stdout: 'subject= /CN=iPhone Developer: Profile 1 (1111AAAA11)/OU=3333CCCC33/O=My Team/C=US', completer: completer, ), ]); Future?> getCodeSigningIdentities() => getCodeSigningIdentityDevelopmentTeamBuildSetting( buildSettings: const {'bogus': 'bogus'}, platform: FakePlatform(operatingSystem: 'macos'), processManager: processManager, logger: BufferLogger.test(), config: Config.test(), terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); await expectLater( () => getCodeSigningIdentities(), throwsA( const TypeMatcher().having( (Exception e) => e.toString(), 'message', equals( 'Exception: Unexpected error when writing to openssl: SocketException: Bad pipe', ), ), ), ); }); }); group('with getCodeSigningIdentityDevelopmentTeam', () { testWithoutContext('does not error when no valid code signing certificates', () async { final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], ), ]); final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: BufferLogger.test(), config: Config.test(), terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(developmentTeam, isNull); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('does not use saved provisioning profile', () async { final testConfig = Config.test(); testConfig.setValue( 'ios-signing-profile', 'Profiles/1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision', ); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), ]); final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: BufferLogger.test(), config: testConfig, terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect(developmentTeam, isNull); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('Test saved certificate used', () async { final testConfig = Config.test(); final logger = BufferLogger.test(); testConfig.setValue('ios-signing-cert', 'iPhone Developer: Profile 3 (3333CCCC33)'); final completer = Completer(); final controller = StreamController>(); final 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 certificate value is passed into openssl command. String? stdin; controller.stream.listen((List chunk) { stdin = utf8.decode(chunk); completer.complete(); }); final String? developmentTeam = await getCodeSigningIdentityDevelopmentTeam( processManager: processManager, platform: FakePlatform(operatingSystem: 'macos'), logger: logger, config: testConfig, terminal: FakeTerminal(), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), plistParser: FakePlistParser(), ); expect( logger.statusText, contains( 'Found saved certificate choice "iPhone Developer: Profile 3 (3333CCCC33)". To clear, use "flutter config --clear-ios-signing-settings"', ), ); expect( logger.statusText, contains( 'Developer identity "iPhone Developer: Profile 3 (3333CCCC33)" selected for iOS code signing', ), ); expect(logger.errorText, isEmpty); expect(stdin, 'This is a fake certificate'); expect(developmentTeam, '4444DDDD44'); expect(processManager, hasNoRemainingExpectations); }); }); }); group('Select signing', () { testWithoutContext('cancels if terminal does not have stdin', () async { final logger = BufferLogger.test(); final config = Config.test(); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), processUtils: ProcessUtils(processManager: FakeProcessManager.empty(), logger: logger), terminal: FakeTerminal(stdinHasTerminal: false), plistParser: FakePlistParser(), ); await settings.selectSettings(); expect(logger.errorText, contains('Unable to detect stdin for the terminal')); expect(config.getValue('ios-signing-cert'), isNull); expect(config.getValue('ios-signing-profile'), isNull); }); testWithoutContext('cancels if code-signing tools are not found', () async { final logger = BufferLogger.test(); final config = Config.test(); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), processUtils: ProcessUtils(processManager: FakeProcessManager.empty(), logger: logger), terminal: FakeTerminal(), plistParser: FakePlistParser(), ); await settings.selectSettings(); expect(logger.errorText, contains('Unable to validate code-signing tools')); expect(config.getValue('ios-signing-cert'), isNull); expect(config.getValue('ios-signing-profile'), isNull); }); testWithoutContext('cancels if signing cert already saved', () async { final logger = BufferLogger.test(); final config = Config.test(); config.setValue('ios-signing-cert', 'some value'); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), ]); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), processUtils: ProcessUtils(processManager: processManager, logger: logger), terminal: FakeTerminal(), plistParser: FakePlistParser(), ); await settings.selectSettings(); expect(logger.errorText, contains('Code-signing settings are already set')); expect(config.getValue('ios-signing-cert'), 'some value'); expect(config.getValue('ios-signing-profile'), isNull); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('cancels if signing profile already saved', () async { final logger = BufferLogger.test(); final config = Config.test(); config.setValue('ios-signing-profile', 'some value'); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), ]); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), processUtils: ProcessUtils(processManager: processManager, logger: logger), terminal: FakeTerminal(), plistParser: FakePlistParser(), ); await settings.selectSettings(); expect(logger.errorText, contains('Code-signing settings are already set')); expect(config.getValue('ios-signing-cert'), isNull); expect(config.getValue('ios-signing-profile'), 'some value'); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('cancels if quit while selecting code signing style', () async { final logger = BufferLogger.test(); final config = Config.test(); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), ]); final terminal = FakeTerminal(); terminal.setPrompt(['1', '2', 'q'], 'q'); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), processUtils: ProcessUtils(processManager: processManager, logger: logger), terminal: terminal, plistParser: FakePlistParser(), ); await settings.selectSettings(); expect( logger.warningText, contains('Code-signing setup canceled. Your changes have not been saved.'), ); expect(config.getValue('ios-signing-cert'), isNull); expect(config.getValue('ios-signing-profile'), isNull); expect(processManager, hasNoRemainingExpectations); }); group('with automatic code signing style', () { testWithoutContext('cancels if no identities are found', () async { final logger = BufferLogger.test(); final config = Config.test(); final terminal = FakeTerminal(); terminal.setPrompt(['1', '2', 'q'], '1'); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], ), ]); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), processUtils: ProcessUtils(processManager: processManager, logger: logger), terminal: terminal, plistParser: FakePlistParser(), ); await settings.selectSettings(); expect(logger.errorText, contains(noCertificatesInstruction())); expect( logger.warningText, contains('Code-signing setup canceled. Your changes have not been saved.'), ); expect(config.getValue('ios-signing-cert'), isNull); expect(config.getValue('ios-signing-profile'), isNull); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('cancels if quit while selecting identity', () async { final logger = BufferLogger.test(); final config = Config.test(); final terminal = FakeTerminal(); terminal.setPrompt(['1', '2', 'q'], '1'); unawaited( terminal.promptCompleter.future.whenComplete(() { terminal.setPrompt(['1', '2', '3', 'q'], 'q'); }), ); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), ]); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), processUtils: ProcessUtils(processManager: processManager, logger: logger), terminal: terminal, plistParser: FakePlistParser(), ); await settings.selectSettings(); expect( logger.warningText, contains('Code-signing setup canceled. Your changes have not been saved.'), ); expect(config.getValue('ios-signing-cert'), isNull); expect(config.getValue('ios-signing-profile'), isNull); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('saves to config after selection', () async { final logger = BufferLogger.test(); final config = Config.test(); final terminal = FakeTerminal(); terminal.setPrompt(['1', '2', 'q'], '1'); unawaited( terminal.promptCompleter.future.whenComplete(() { terminal.setPrompt(['1', '2', '3', 'q'], '3'); }), ); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: kCertificates, ), ]); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), processUtils: ProcessUtils(processManager: processManager, logger: logger), terminal: terminal, plistParser: FakePlistParser(), ); await settings.selectSettings(); expect(logger.warningText, isEmpty); expect(config.getValue('ios-signing-cert'), 'iPhone Developer: Profile 3 (3333CCCC33)'); expect(config.getValue('ios-signing-profile'), isNull); expect(processManager, hasNoRemainingExpectations); }); }); group('with manual code signing style', () { testWithoutContext('cancels if no profiles are found', () async { final logger = BufferLogger.test(); final config = Config.test(); final terminal = FakeTerminal(); terminal.setPrompt(['1', '2', 'q'], '2'); final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), ]); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: MemoryFileSystem.test(), fileSystemUtils: FakeFileSystemUtils(), processUtils: ProcessUtils(processManager: processManager, logger: logger), terminal: terminal, plistParser: FakePlistParser(), ); await settings.selectSettings(); expect(logger.errorText, contains('No provisioning profiles were found')); expect( logger.warningText, contains('Code-signing setup canceled. Your changes have not been saved.'), ); expect(config.getValue('ios-signing-cert'), isNull); expect(config.getValue('ios-signing-profile'), isNull); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('cancels if quit while selecting profile', () async { final logger = BufferLogger.test(); final config = Config.test(); final fileSystem = MemoryFileSystem.test(); final terminal = FakeTerminal(); terminal.setPrompt(['1', '2', 'q'], '2'); unawaited( terminal.promptCompleter.future.whenComplete(() { terminal.setPrompt(['1', '2', 'q'], 'q'); }), ); const homeDir = '/Users/username'; final Directory profileDirectory = fileSystem.directory( fileSystem.path.join( homeDir, 'Library', 'Developer', 'Xcode', 'UserData', 'Provisioning Profiles', ), ); final File validProfile = profileDirectory.childFile('profile1.mobileprovision') ..createSync(recursive: true); final File validProfilePlist = fileSystem.file( '/.tmp_rand0/provisioning_profiles/decoded_profile_profile1.mobileprovision.plist', ); final File validProfileCert = fileSystem.file( '/.tmp_rand0/provisioning_profile_certificates/UUIDProfile1_0.cer', ); final plistParser = FakePlistParser( parsedValues: >[ { 'Name': 'Company Development', 'ExpirationDate': '2026-02-20T16:04:31Z', 'IsXcodeManaged': false, 'DeveloperCertificates': >[ [0, 1, 2, 3], ], 'TeamIdentifier': ['ABCDE1F2DH'], 'UUID': 'UUIDProfile1', }, ], ); const certificates = ''' 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "Apple Development: Company Development (12ABCD234E)" 1 valid identities found'''; final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: certificates, ), FakeCommand( command: [ 'security', 'cms', '-D', '-i', validProfile.path, '-o', validProfilePlist.path, ], onRun: (List command) => validProfilePlist.createSync(recursive: true), ), FakeCommand( command: [ 'openssl', 'x509', '-subject', '-in', validProfileCert.path, '-inform', 'DER', ], stdout: 'subject= /UID=A123BC4D5E/CN=Apple Development: Company Development (12ABCD234E)/OU=ABCDE1F2DH/O=Company LLC/C=US', ), ]); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: fileSystem, fileSystemUtils: FakeFileSystemUtils(homeDirPath: homeDir), processUtils: ProcessUtils(processManager: processManager, logger: logger), terminal: terminal, plistParser: plistParser, ); await settings.selectSettings(); expect(logger.errorText, isEmpty); expect( logger.warningText, contains('Code-signing setup canceled. Your changes have not been saved.'), ); expect(config.getValue('ios-signing-cert'), isNull); expect(config.getValue('ios-signing-profile'), isNull); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext( 'cancels if "Other (not listed)" selected while selecting profile', () async { final logger = BufferLogger.test(); final config = Config.test(); final fileSystem = MemoryFileSystem.test(); final terminal = FakeTerminal(); terminal.setPrompt(['1', '2', 'q'], '2'); unawaited( terminal.promptCompleter.future.whenComplete(() { terminal.setPrompt(['1', '2', 'q'], '2'); }), ); const homeDir = '/Users/username'; final Directory profileDirectory = fileSystem.directory( fileSystem.path.join( homeDir, 'Library', 'Developer', 'Xcode', 'UserData', 'Provisioning Profiles', ), ); final File validProfile = profileDirectory.childFile('profile1.mobileprovision') ..createSync(recursive: true); final File validProfilePlist = fileSystem.file( '/.tmp_rand0/provisioning_profiles/decoded_profile_profile1.mobileprovision.plist', ); final File validProfileCert = fileSystem.file( '/.tmp_rand0/provisioning_profile_certificates/UUIDProfile1_0.cer', ); final plistParser = FakePlistParser( parsedValues: >[ { 'Name': 'Company Development', 'ExpirationDate': '2026-02-20T16:04:31Z', 'IsXcodeManaged': false, 'DeveloperCertificates': >[ [0, 1, 2, 3], ], 'TeamIdentifier': ['ABCDE1F2DH'], 'UUID': 'UUIDProfile1', }, ], ); const certificates = ''' 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "Apple Development: Company Development (12ABCD234E)" 1 valid identities found'''; final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: certificates, ), FakeCommand( command: [ 'security', 'cms', '-D', '-i', validProfile.path, '-o', validProfilePlist.path, ], onRun: (List command) => validProfilePlist.createSync(recursive: true), ), FakeCommand( command: [ 'openssl', 'x509', '-subject', '-in', validProfileCert.path, '-inform', 'DER', ], stdout: 'subject= /UID=A123BC4D5E/CN=Apple Development: Company Development (12ABCD234E)/OU=ABCDE1F2DH/O=Company LLC/C=US', ), ]); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: fileSystem, fileSystemUtils: FakeFileSystemUtils(homeDirPath: homeDir), processUtils: ProcessUtils(processManager: processManager, logger: logger), terminal: terminal, plistParser: plistParser, ); await settings.selectSettings(); expect( logger.errorText, contains('If you have already downloaded a provisioning profile'), ); expect( logger.warningText, contains('Code-signing setup canceled. Your changes have not been saved.'), ); expect(config.getValue('ios-signing-cert'), isNull); expect(config.getValue('ios-signing-profile'), isNull); expect(processManager, hasNoRemainingExpectations); }, ); testWithoutContext('saves to config after selecting', () async { final logger = BufferLogger.test(); final config = Config.test(); final fileSystem = MemoryFileSystem.test(); final terminal = FakeTerminal(); terminal.setPrompt(['1', '2', 'q'], '2'); unawaited( terminal.promptCompleter.future.whenComplete(() { terminal.setPrompt(['1', '2', 'q'], '1'); }), ); const homeDir = '/Users/username'; final Directory profileDirectory = fileSystem.directory( fileSystem.path.join( homeDir, 'Library', 'Developer', 'Xcode', 'UserData', 'Provisioning Profiles', ), ); final File validProfile = profileDirectory.childFile('profile1.mobileprovision') ..createSync(recursive: true); final File validProfilePlist = fileSystem.file( '/.tmp_rand0/provisioning_profiles/decoded_profile_profile1.mobileprovision.plist', ); final File validProfileInvalidCert = fileSystem.file( '/.tmp_rand0/provisioning_profile_certificates/UUIDProfile1_0.cer', ); final File validProfileValidCert = fileSystem.file( '/.tmp_rand0/provisioning_profile_certificates/UUIDProfile1_1.cer', ); final File xcodeManagedProfile = profileDirectory.childFile('profile2.mobileprovision') ..createSync(recursive: true); final File xcodeManagedProfilePlist = fileSystem.file( '/.tmp_rand0/provisioning_profiles/decoded_profile_profile2.mobileprovision.plist', ); final File profileWithMissingIdentity = profileDirectory.childFile( 'profile3.mobileprovision', )..createSync(recursive: true); final File profileWithMissingIdentityPlist = fileSystem.file( '/.tmp_rand0/provisioning_profiles/decoded_profile_profile3.mobileprovision.plist', ); final File profileWithMissingIdentityCert = fileSystem.file( '/.tmp_rand0/provisioning_profile_certificates/UUIDProfile3_0.cer', ); final plistParser = FakePlistParser( parsedValues: >[ { 'Name': 'Company Development', 'ExpirationDate': '2026-02-20T16:04:31Z', 'IsXcodeManaged': false, 'DeveloperCertificates': >[ [0, 1, 2, 3], [0, 1, 2, 3, 4], ], 'TeamIdentifier': ['ABCDE1F2DH'], 'UUID': 'UUIDProfile1', }, { 'Name': 'Company Development', 'ExpirationDate': '2026-02-20T16:04:31Z', 'IsXcodeManaged': true, 'DeveloperCertificates': >[ [0, 1, 2, 3], ], 'TeamIdentifier': ['ABCDE1F2DH'], 'UUID': 'UUIDProfile2', }, { 'Name': 'Company Development', 'ExpirationDate': '2026-02-20T16:04:31Z', 'IsXcodeManaged': false, 'DeveloperCertificates': >[ [0, 1, 2, 3], ], 'TeamIdentifier': ['ABCDE1F2DH'], 'UUID': 'UUIDProfile3', }, ], ); const certificates = ''' 1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "Apple Development: Company Development (12ABCD234E)" 1 valid identities found'''; final processManager = FakeProcessManager.list([ const FakeCommand(command: ['which', 'security']), const FakeCommand(command: ['which', 'openssl']), const FakeCommand( command: ['security', 'find-identity', '-p', 'codesigning', '-v'], stdout: certificates, ), FakeCommand( command: [ 'security', 'cms', '-D', '-i', validProfile.path, '-o', validProfilePlist.path, ], onRun: (List command) => validProfilePlist.createSync(recursive: true), ), FakeCommand( command: [ 'openssl', 'x509', '-subject', '-in', validProfileInvalidCert.path, '-inform', 'DER', ], stdout: 'subject= /UID=A123BC4D5E/CN=Apple Development: No matching (12ABCD234E)/OU=ABCDE1F2DH/O=Company LLC/C=US', ), FakeCommand( command: [ 'openssl', 'x509', '-subject', '-in', validProfileValidCert.path, '-inform', 'DER', ], stdout: 'subject= /UID=A123BC4D5E/CN=Apple Development: Company Development (12ABCD234E)/OU=ABCDE1F2DH/O=Company LLC/C=US', ), FakeCommand( command: [ 'security', 'cms', '-D', '-i', xcodeManagedProfile.path, '-o', xcodeManagedProfilePlist.path, ], onRun: (List command) => xcodeManagedProfilePlist.createSync(recursive: true), ), FakeCommand( command: [ 'security', 'cms', '-D', '-i', profileWithMissingIdentity.path, '-o', profileWithMissingIdentityPlist.path, ], onRun: (List command) => profileWithMissingIdentityPlist.createSync(recursive: true), ), FakeCommand( command: [ 'openssl', 'x509', '-subject', '-in', profileWithMissingIdentityCert.path, '-inform', 'DER', ], stdout: 'subject= /UID=A123BC4D5E/CN=Apple Development: No match (12ABCD234E)/OU=ABCDE1F2DH/O=Company LLC/C=US', ), ]); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: fileSystem, fileSystemUtils: FakeFileSystemUtils(homeDirPath: homeDir), processUtils: ProcessUtils(processManager: processManager, logger: logger), terminal: terminal, plistParser: plistParser, ); await settings.selectSettings(); expect(logger.errorText, isEmpty); expect(logger.warningText, isEmpty); expect(config.getValue('ios-signing-cert'), isNull); expect(config.getValue('ios-signing-profile'), validProfile.path); expect(processManager, hasNoRemainingExpectations); }); }); }); testWithoutContext('getProvisioningProfileFromConfig', () async { final logger = BufferLogger.test(); final config = Config.test(); final fileSystem = MemoryFileSystem.test(); const profileBaseName = '1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision'; const profileFilePath = '/path/to/profiles/$profileBaseName'; fileSystem.file(profileFilePath).createSync(recursive: true); config.setValue('ios-signing-profile', profileFilePath); final File profilePlist = fileSystem.file( '/.tmp_rand0/provisioning_profiles/decoded_profile_1234567a-bcde-89f0-1234-g56hi567j8kl.mobileprovision.plist', ); final File cert = fileSystem.file( '/.tmp_rand0/provisioning_profile_certificates/UUID1234_0.cer', ); final plistParser = FakePlistParser( parsedValues: >[ { 'Name': 'Company Development', 'ExpirationDate': '2026-02-20T16:04:31Z', 'IsXcodeManaged': false, 'DeveloperCertificates': >[ [0, 1, 2, 3], ], 'TeamIdentifier': ['ABCDE1F2DH'], 'UUID': 'UUID1234', }, ], ); final processManager = FakeProcessManager.list([ FakeCommand( command: const [ 'security', 'cms', '-D', '-i', profileFilePath, '-o', '/.tmp_rand0/provisioning_profiles/decoded_profile_$profileBaseName.plist', ], onRun: (List command) => profilePlist.createSync(recursive: true), ), FakeCommand( command: ['openssl', 'x509', '-subject', '-in', cert.path, '-inform', 'DER'], stdout: 'subject= /UID=A123BC4D5E/CN=Apple Development: Company Development (12ABCD234E)/OU=ABCDE1F2DH/O=Company LLC/C=US', ), ]); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: fileSystem, fileSystemUtils: FakeFileSystemUtils(), processUtils: ProcessUtils(processManager: processManager, logger: logger), terminal: FakeTerminal(stdinHasTerminal: false), plistParser: plistParser, ); final validCodeSigningIdentities = [ 'Apple Development: Company Development (12ABCD234E)', ]; expect(await settings.getProvisioningProfileFromConfig(validCodeSigningIdentities), isNotNull); expect(logger.errorText, isEmpty); expect(logger.warningText, isEmpty); expect(processManager, hasNoRemainingExpectations); }); testWithoutContext('getIdentityFromCertFromConfig', () async { final logger = BufferLogger.test(); final config = Config.test(); final fileSystem = MemoryFileSystem.test(); config.setValue('ios-signing-cert', 'Apple Development: Company Development (12ABCD234E)'); final settings = XcodeCodeSigningSettings( config: config, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), fileSystem: fileSystem, fileSystemUtils: FakeFileSystemUtils(), processUtils: ProcessUtils(processManager: FakeProcessManager.empty(), logger: logger), terminal: FakeTerminal(stdinHasTerminal: false), plistParser: FakePlistParser(), ); final validCodeSigningIdentities = [ 'Apple Development: Company Development (12ABCD234E)', ]; expect(await settings.getIdentityFromCertFromConfig(validCodeSigningIdentities), isNotNull); expect(logger.errorText, isEmpty); expect(logger.warningText, isEmpty); }); } class FakeTerminal extends Fake implements AnsiTerminal { FakeTerminal({this.stdinHasTerminal = true, this.supportsColor = false}); @override final bool stdinHasTerminal; @override final bool supportsColor; @override bool get isCliAnimationEnabled => supportsColor; @override bool usesTerminalUi = true; @override bool singleCharMode = false; late Completer promptCompleter; void setPrompt(List characters, String result) { _nextPrompt = characters; _nextResult = result; promptCompleter = Completer(); } List? _nextPrompt; late String _nextResult; @override Future promptForCharInput( List acceptedCharacters, { Logger? logger, String? prompt, int? defaultChoiceIndex, bool displayAcceptedCharacters = true, }) async { expect(acceptedCharacters, _nextPrompt); promptCompleter.complete(); return _nextResult; } } class FakeFileSystemUtils extends Fake implements FileSystemUtils { FakeFileSystemUtils({this.homeDirPath}); @override String? homeDirPath; } class FakePlistParser extends Fake implements PlistParser { FakePlistParser({List>? parsedValues}) : _parsedValues = parsedValues ?? >[]; final List> _parsedValues; @override Map parseFile(String plistFilePath) { if (_parsedValues.isEmpty) { return {}; } return _parsedValues.removeAt(0); } }