[macOS] support secure restorable state by default (#151605)
By default, Flutter apps only do default AppKit app serialisation of Window location etc. and by default, state serialisation in AppKit apps is compatible with `NSSecureCoding`. AppKit apps generated since Xcode 13.2 include this method in the app delegate generated by the default app template. Background ========== This method was added to opt into having [de]serialization require a coder implementing the `NSSecureCoding` protocol. Apple wasn't able to force this across the board, because `NSSecureCoding` limits certain behaviours during deserialisation, which some third-party apps have have previously relied on. Specific background on the sorts of vulnerabilities that `NSSecureCoding` was designed to prevent are described in the `NSSecureCoding` documentation: https://developer.apple.com/documentation/foundation/nssecurecoding?language=objc A demonstration of a root privilege escalation and SIP bypass vulnerability is described in the following blog post: https://sector7.computest.nl/post/2022-08-process-injection-breaking-all-macos-security-layers-with-a-single-vulnerability/ Fixes: https://github.com/flutter/flutter/issues/150062 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
e47c837290
commit
68f375fe38
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ import 'migrations/flutter_application_migration.dart';
|
|||||||
import 'migrations/macos_deployment_target_migration.dart';
|
import 'migrations/macos_deployment_target_migration.dart';
|
||||||
import 'migrations/nsapplicationmain_deprecation_migration.dart';
|
import 'migrations/nsapplicationmain_deprecation_migration.dart';
|
||||||
import 'migrations/remove_macos_framework_link_and_embedding_migration.dart';
|
import 'migrations/remove_macos_framework_link_and_embedding_migration.dart';
|
||||||
|
import 'migrations/secure_restorable_state_migration.dart';
|
||||||
|
|
||||||
/// When run in -quiet mode, Xcode should only print from the underlying tasks to stdout.
|
/// When run in -quiet mode, Xcode should only print from the underlying tasks to stdout.
|
||||||
/// Passing this regexp to trace moves the stdout output to stderr.
|
/// Passing this regexp to trace moves the stdout output to stderr.
|
||||||
@ -87,6 +88,7 @@ Future<void> buildMacOS({
|
|||||||
XcodeThinBinaryBuildPhaseInputPathsMigration(flutterProject.macos, globals.logger),
|
XcodeThinBinaryBuildPhaseInputPathsMigration(flutterProject.macos, globals.logger),
|
||||||
FlutterApplicationMigration(flutterProject.macos, globals.logger),
|
FlutterApplicationMigration(flutterProject.macos, globals.logger),
|
||||||
NSApplicationMainDeprecationMigration(flutterProject.macos, globals.logger),
|
NSApplicationMainDeprecationMigration(flutterProject.macos, globals.logger),
|
||||||
|
SecureRestorableStateMigration(flutterProject.macos, globals.logger),
|
||||||
if (flutterProject.usesSwiftPackageManager && flutterProject.macos.flutterPluginSwiftPackageManifest.existsSync())
|
if (flutterProject.usesSwiftPackageManager && flutterProject.macos.flutterPluginSwiftPackageManifest.existsSync())
|
||||||
SwiftPackageManagerIntegrationMigration(
|
SwiftPackageManagerIntegrationMigration(
|
||||||
flutterProject.macos,
|
flutterProject.macos,
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
// 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 '../../base/file_system.dart';
|
||||||
|
import '../../base/project_migrator.dart';
|
||||||
|
import '../../xcode_project.dart';
|
||||||
|
|
||||||
|
const String _appDelegateFileBefore = r'''
|
||||||
|
class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}''';
|
||||||
|
|
||||||
|
const String _appDelegateFileAfter = r'''
|
||||||
|
class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}''';
|
||||||
|
|
||||||
|
/// Add `applicationSupportsSecureRestorableState` if not already present.
|
||||||
|
///
|
||||||
|
/// In all new AppKit apps since Xcode 13.2, the AppDelegate template includes
|
||||||
|
/// this method, which opts in to requiring safe deserialization via the
|
||||||
|
/// `NSSecureCoding` protocol. Because this required new API, existing apps
|
||||||
|
/// need to opt in to this behavior.
|
||||||
|
///
|
||||||
|
/// Since nearly all Flutter macOS apps will be doing serialization of Flutter
|
||||||
|
/// state via Dart code, it's a very safe bet that the vast majority of
|
||||||
|
/// existing Flutter apps can safely enable this flag. The few apps that
|
||||||
|
/// are doing serialization via older insecure APIs can update the migrated
|
||||||
|
/// code to return false.
|
||||||
|
///
|
||||||
|
/// See:
|
||||||
|
/// https://developer.apple.com/documentation/foundation/nssecurecoding?language=objc
|
||||||
|
class SecureRestorableStateMigration extends ProjectMigrator {
|
||||||
|
SecureRestorableStateMigration(
|
||||||
|
MacOSProject project,
|
||||||
|
super.logger,
|
||||||
|
) : _appDelegateSwift = project.appDelegateSwift;
|
||||||
|
|
||||||
|
final File _appDelegateSwift;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> migrate() async {
|
||||||
|
// Skip this migration if the project uses Objective-C.
|
||||||
|
if (!_appDelegateSwift.existsSync()) {
|
||||||
|
logger.printTrace(
|
||||||
|
'macos/Runner/AppDelegate.swift not found. Skipping applicationSupportsSecureRestorableState migration.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String original = _appDelegateSwift.readAsStringSync();
|
||||||
|
|
||||||
|
// If we have an AppDelegate.swift, but can't migrate, log a warning.
|
||||||
|
if (!original.contains(_appDelegateFileBefore)) {
|
||||||
|
if (original.contains('applicationSupportsSecureRestorableState')) {
|
||||||
|
// User has already overridden this method. Exit quietly.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.printWarning('''
|
||||||
|
macos/Runner/AppDelegate.swift has been modified and cannot be automatically migrated.
|
||||||
|
We recommend developers override applicationSupportsSecureRestorableState in AppDelegate.swift as follows:
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate the macos/Runner/AppDelegate.swift file.
|
||||||
|
final String migrated = original.replaceFirst(_appDelegateFileBefore, _appDelegateFileAfter);
|
||||||
|
if (original == migrated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.printWarning(
|
||||||
|
'macos/Runner/AppDelegate.swift does not override applicationSupportsSecureRestorableState. Updating.'
|
||||||
|
);
|
||||||
|
_appDelegateSwift.writeAsStringSync(migrated);
|
||||||
|
}
|
||||||
|
}
|
@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import 'package:flutter_tools/src/macos/migrations/flutter_application_migration
|
|||||||
import 'package:flutter_tools/src/macos/migrations/macos_deployment_target_migration.dart';
|
import 'package:flutter_tools/src/macos/migrations/macos_deployment_target_migration.dart';
|
||||||
import 'package:flutter_tools/src/macos/migrations/nsapplicationmain_deprecation_migration.dart';
|
import 'package:flutter_tools/src/macos/migrations/nsapplicationmain_deprecation_migration.dart';
|
||||||
import 'package:flutter_tools/src/macos/migrations/remove_macos_framework_link_and_embedding_migration.dart';
|
import 'package:flutter_tools/src/macos/migrations/remove_macos_framework_link_and_embedding_migration.dart';
|
||||||
|
import 'package:flutter_tools/src/macos/migrations/secure_restorable_state_migration.dart';
|
||||||
import 'package:flutter_tools/src/project.dart';
|
import 'package:flutter_tools/src/project.dart';
|
||||||
import 'package:flutter_tools/src/reporting/reporting.dart';
|
import 'package:flutter_tools/src/reporting/reporting.dart';
|
||||||
import 'package:test/fake.dart';
|
import 'package:test/fake.dart';
|
||||||
@ -487,6 +488,160 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
expect(testLogger.warningText, contains('uses the deprecated @NSApplicationMain attribute, updating'));
|
expect(testLogger.warningText, contains('uses the deprecated @NSApplicationMain attribute, updating'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('migrate AppDelegate to override applicationSupportsSecureRestorableState', () {
|
||||||
|
late MemoryFileSystem memoryFileSystem;
|
||||||
|
late BufferLogger testLogger;
|
||||||
|
late FakeMacOSProject project;
|
||||||
|
late File appDelegateFile;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
memoryFileSystem = MemoryFileSystem();
|
||||||
|
testLogger = BufferLogger.test();
|
||||||
|
project = FakeMacOSProject();
|
||||||
|
appDelegateFile = memoryFileSystem.file('AppDelegate.swift');
|
||||||
|
project.appDelegateSwift = appDelegateFile;
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('skipped if files are missing', () async {
|
||||||
|
final SecureRestorableStateMigration migration = SecureRestorableStateMigration(
|
||||||
|
project,
|
||||||
|
testLogger,
|
||||||
|
);
|
||||||
|
await migration.migrate();
|
||||||
|
expect(appDelegateFile.existsSync(), isFalse);
|
||||||
|
|
||||||
|
expect(testLogger.statusText, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('skipped if nothing to upgrade', () async {
|
||||||
|
const String appDelegateContents = '''
|
||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
appDelegateFile.writeAsStringSync(appDelegateContents);
|
||||||
|
final DateTime lastModified = appDelegateFile.lastModifiedSync();
|
||||||
|
|
||||||
|
final SecureRestorableStateMigration migration = SecureRestorableStateMigration(
|
||||||
|
project,
|
||||||
|
testLogger,
|
||||||
|
);
|
||||||
|
await migration.migrate();
|
||||||
|
|
||||||
|
expect(appDelegateFile.lastModifiedSync(), lastModified);
|
||||||
|
expect(appDelegateFile.readAsStringSync(), appDelegateContents);
|
||||||
|
|
||||||
|
expect(testLogger.statusText, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('skipped if override already present, but different', () async {
|
||||||
|
const String appDelegateContents = '''
|
||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
appDelegateFile.writeAsStringSync(appDelegateContents);
|
||||||
|
final DateTime lastModified = appDelegateFile.lastModifiedSync();
|
||||||
|
|
||||||
|
final SecureRestorableStateMigration migration = SecureRestorableStateMigration(
|
||||||
|
project,
|
||||||
|
testLogger,
|
||||||
|
);
|
||||||
|
await migration.migrate();
|
||||||
|
|
||||||
|
expect(appDelegateFile.lastModifiedSync(), lastModified);
|
||||||
|
expect(appDelegateFile.readAsStringSync(), appDelegateContents);
|
||||||
|
|
||||||
|
expect(testLogger.statusText, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('warns if override not present and cannot be applied cleanly', () async {
|
||||||
|
const String appDelegateContents = '''
|
||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: FlutterAppDelegate {
|
||||||
|
let ninetiesSong = "島人ぬ宝"
|
||||||
|
|
||||||
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
appDelegateFile.writeAsStringSync(appDelegateContents);
|
||||||
|
final DateTime lastModified = appDelegateFile.lastModifiedSync();
|
||||||
|
|
||||||
|
final SecureRestorableStateMigration migration = SecureRestorableStateMigration(
|
||||||
|
project,
|
||||||
|
testLogger,
|
||||||
|
);
|
||||||
|
await migration.migrate();
|
||||||
|
|
||||||
|
expect(appDelegateFile.lastModifiedSync(), lastModified);
|
||||||
|
expect(appDelegateFile.readAsStringSync(), appDelegateContents);
|
||||||
|
|
||||||
|
expect(testLogger.warningText, contains('has been modified and cannot be automatically migrated.'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('updates AppDelegate.swift', () async {
|
||||||
|
appDelegateFile.writeAsStringSync('''
|
||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
|
||||||
|
final SecureRestorableStateMigration migration = SecureRestorableStateMigration(
|
||||||
|
project,
|
||||||
|
testLogger,
|
||||||
|
);
|
||||||
|
await migration.migrate();
|
||||||
|
|
||||||
|
expect(appDelegateFile.readAsStringSync(), '''
|
||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''');
|
||||||
|
expect(testLogger.warningText, contains('does not override applicationSupportsSecureRestorableState. Updating.'));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeMacOSProject extends Fake implements MacOSProject {
|
class FakeMacOSProject extends Fake implements MacOSProject {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user