diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index b2eefa7a..38935ed1 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -83,7 +83,7 @@ jobs: test_macos: runs-on: macos-latest - timeout-minutes: 25 + timeout-minutes: 40 environment: CI Environment env: FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} @@ -107,6 +107,11 @@ jobs: - uses: actions/checkout@v2 with: ref: ${{ github.event.pull_request.head.sha || github.ref }} + - name: Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer + - uses: futureware-tech/simulator-action@v4 + with: + model: 'iPhone 16' - uses: subosito/flutter-action@v2 with: channel: 'stable' diff --git a/README.md b/README.md index c678276a..e0193612 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,11 @@ If you wish to be specific about which platforms you want to configure, use the flutterfire configure --yes --project= --platforms=android,ios,web ``` +If you want to specify a display name (i.e. how it appears in the Firebase console), use the `--display-name` flag. It will still have the platform as a suffix (e.g. `--display-name=test` will appear as "test (ios), "test (web)", etc"): +```bash +flutterfire configure --yes --project= --display-name=test +``` + FlutterFire CLI now supports Windows applications. The Firebase console does not allow you to create an application for Windows, FlutterFire CLI will create a **web app** if you specify windows as a platform you want to configure: ```bash diff --git a/packages/flutterfire_cli/lib/src/commands/config.dart b/packages/flutterfire_cli/lib/src/commands/config.dart index 5b2b5d19..92d3078b 100644 --- a/packages/flutterfire_cli/lib/src/commands/config.dart +++ b/packages/flutterfire_cli/lib/src/commands/config.dart @@ -62,6 +62,14 @@ class ConfigCommand extends FlutterFireCommand { 'Optionally specify the platforms to generate configuration options for ' 'as a comma separated list. For example "android,ios,macos,web,linux,windows".', ); + argParser.addOption( + kDisplayNameFlag, + valueHelp: 'displayName', + abbr: 'n', + help: 'The display name of your app, e.g. "My Cool App". ' + 'If no display name is provided then an attempt will be made to ' + 'automatically detect it from your project configuration (if it exists).', + ); argParser.addOption( kIosBundleIdFlag, valueHelp: 'bundleIdentifier', @@ -243,6 +251,11 @@ class ConfigCommand extends FlutterFireCommand { .toList(); } + String get displayName { + final name = argResults![kDisplayNameFlag] as String?; + return name ?? flutterApp!.package.pubSpec.name; + } + bool get applyGradlePlugins { return argResults!['apply-gradle-plugins'] as bool; } @@ -625,6 +638,7 @@ class ConfigCommand extends FlutterFireCommand { iosBundleId: iosBundleId, macosBundleId: macosBundleId, token: token, + displayName: displayName, serviceAccount: serviceAccount, webAppId: webAppId, windowsAppId: windowsAppId, diff --git a/packages/flutterfire_cli/lib/src/common/utils.dart b/packages/flutterfire_cli/lib/src/common/utils.dart index b99caf47..169aacab 100644 --- a/packages/flutterfire_cli/lib/src/common/utils.dart +++ b/packages/flutterfire_cli/lib/src/common/utils.dart @@ -63,6 +63,7 @@ const String kConfigurations = 'configurations'; const String kOutFlag = 'out'; const String kYesFlag = 'yes'; const String kPlatformsFlag = 'platforms'; +const String kDisplayNameFlag = 'display-name'; const String kIosBundleIdFlag = 'ios-bundle-id'; const String kMacosBundleIdFlag = 'macos-bundle-id'; const String kAndroidAppIdFlag = 'android-app-id'; diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_android_options.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_android_options.dart index a66e7c99..aeadeb30 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_android_options.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_android_options.dart @@ -40,6 +40,7 @@ extension FirebaseAndroidOptions on FirebaseOptions { String? firebaseAccount, required String? token, required String? serviceAccount, + required String displayName, }) async { var selectedAndroidApplicationId = androidApplicationId ?? flutterApp.androidApplicationId; @@ -56,7 +57,7 @@ extension FirebaseAndroidOptions on FirebaseOptions { ); final firebaseApp = await firebase.findOrCreateFirebaseApp( packageNameOrBundleIdentifier: selectedAndroidApplicationId, - displayName: flutterApp.package.pubSpec.name, + displayName: displayName, platform: kAndroid, project: firebaseProjectId, account: firebaseAccount, diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_apple_options.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_apple_options.dart index 36debd88..d6fde9ce 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_apple_options.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_apple_options.dart @@ -33,6 +33,7 @@ extension FirebaseAppleOptions on FirebaseOptions { String? firebaseAccount, required String? token, required String? serviceAccount, + required String displayName, }) async { final platformIdentifier = macos ? kMacos : kIos; var selectedAppleBundleId = appleBundleIdentifier ?? @@ -58,7 +59,7 @@ extension FirebaseAppleOptions on FirebaseOptions { ); final firebaseApp = await firebase.findOrCreateFirebaseApp( packageNameOrBundleIdentifier: selectedAppleBundleId, - displayName: flutterApp.package.pubSpec.name, + displayName: displayName, platform: platformIdentifier, project: firebaseProjectId, account: firebaseAccount, diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_dart_options.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_dart_options.dart index edadd712..3526e586 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_dart_options.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_dart_options.dart @@ -32,9 +32,10 @@ extension FirebaseDartOptions on FirebaseOptions { String platform = kWeb, required String? token, required String? serviceAccount, + required String displayName, }) async { final firebaseApp = await firebase.findOrCreateFirebaseApp( - displayName: flutterApp.package.pubSpec.name, + displayName: displayName, platform: platform, project: firebaseProjectId, account: firebaseAccount, diff --git a/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart b/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart index 1a9d50b4..09d063c2 100644 --- a/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart +++ b/packages/flutterfire_cli/lib/src/firebase/firebase_platform_options.dart @@ -40,6 +40,7 @@ Future fetchAllFirebaseOptions({ String? windowsAppId, String? token, String? serviceAccount, + required String displayName, }) async { FirebaseOptions? androidOptions; FirebaseOptions? iosOptions; @@ -56,6 +57,7 @@ Future fetchAllFirebaseOptions({ firebaseAccount: firebaseAccount, token: token, serviceAccount: serviceAccount, + displayName: displayName, ); } @@ -67,6 +69,7 @@ Future fetchAllFirebaseOptions({ firebaseAccount: firebaseAccount, token: token, serviceAccount: serviceAccount, + displayName: displayName, ); } if (macos) { @@ -78,6 +81,7 @@ Future fetchAllFirebaseOptions({ macos: true, token: token, serviceAccount: serviceAccount, + displayName: displayName, ); } @@ -89,6 +93,7 @@ Future fetchAllFirebaseOptions({ webAppId: webAppId, token: token, serviceAccount: serviceAccount, + displayName: displayName, ); } @@ -101,6 +106,7 @@ Future fetchAllFirebaseOptions({ token: token, webAppId: windowsAppId, serviceAccount: serviceAccount, + displayName: displayName, ); } @@ -112,6 +118,7 @@ Future fetchAllFirebaseOptions({ platform: kLinux, token: token, serviceAccount: serviceAccount, + displayName: displayName, ); } diff --git a/packages/flutterfire_cli/test/configure_test.dart b/packages/flutterfire_cli/test/configure_test.dart index ba5cd9e5..70cb6b3b 100644 --- a/packages/flutterfire_cli/test/configure_test.dart +++ b/packages/flutterfire_cli/test/configure_test.dart @@ -9,521 +9,1323 @@ import 'package:test/test.dart'; import 'test_utils.dart'; void main() { - String? projectPath; - setUp(() async { - projectPath = await createFlutterProject(); - }); - - tearDown(() { - Directory(p.dirname(projectPath!)).delete(recursive: true); - }); + // Default group for tests that don't need iOS 15.0 + group('FlutterFire configure tests - no build', () { + String? projectPath; + setUp(() async { + projectPath = await createFlutterProject(); + }); + + tearDown(() { + Directory(p.dirname(projectPath!)).delete(recursive: true); + }); + + test( + 'flutterfire configure: android - "default" Apple - "default"', + () async { + // the most basic 'flutterfire configure' command that can be run without command line prompts + const defaultTarget = 'Runner'; + final result = Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + '--platforms=android,ios,macos,web,windows', + '--web-app-id=$webAppId', + '--windows-app-id=$windowsAppId', + '--project=$firebaseProjectId', + ], + workingDirectory: projectPath, + runInShell: true, + ); - test( - 'flutterfire configure: android - "default" Apple - "default"', - () async { - // the most basic 'flutterfire configure' command that can be run without command line prompts - const defaultTarget = 'Runner'; - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--platforms=android,ios,macos,web,windows', - '--web-app-id=$webAppId', - '--windows-app-id=$windowsAppId', - '--project=$firebaseProjectId', - ], - workingDirectory: projectPath, - runInShell: true, - ); + if (result.exitCode != 0) { + fail(result.stderr as String); + } - if (result.exitCode != 0) { - fail(result.stderr as String); - } + if (Platform.isMacOS) { + // check Apple service files were created and have correct content + final iosPath = + p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); + final macosPath = p.join(projectPath!, kMacos, defaultTarget); + + await testAppleServiceFileValues(iosPath); + await testAppleServiceFileValues( + macosPath, + platform: kMacos, + ); + + // check default "firebase.json" was created and has correct content + final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); + final firebaseJsonFileContent = + await File(firebaseJsonFile).readAsString(); + + final decodedFirebaseJson = + jsonDecode(firebaseJsonFileContent) as Map; + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [kFlutter, kPlatforms, kIos, kDefaultConfig], + '$kIos/$defaultTarget/$appleServiceFileName', + ); + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kMacos, + kDefaultConfig, + ], + '$kMacos/$defaultTarget/$appleServiceFileName', + ); + + checkAndroidFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kAndroid, + kDefaultConfig, + ], + 'android/app/$androidServiceFileName', + ); + + const defaultFilePath = 'lib/firebase_options.dart'; + final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; + + checkDartFirebaseJsonValues( + decodedFirebaseJson, + keysToMapDart, + ); + + // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files + final iosXcodeProject = p.join( + projectPath!, + kIos, + 'Runner.xcodeproj', + ); - if (Platform.isMacOS) { - // check Apple service files were created and have correct content - final iosPath = - p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); - final macosPath = p.join(projectPath!, kMacos, defaultTarget); + final scriptToCheckIosPbxprojFile = + rubyScriptForTestingDefaultConfigure(iosXcodeProject); - await testAppleServiceFileValues(iosPath); - await testAppleServiceFileValues( - macosPath, - platform: kMacos, - ); + final iosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckIosPbxprojFile, + ], + runInShell: true, + ); - // check default "firebase.json" was created and has correct content - final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); - final firebaseJsonFileContent = - await File(firebaseJsonFile).readAsString(); + if (iosResult.exitCode != 0) { + fail(iosResult.stderr as String); + } - final decodedFirebaseJson = - jsonDecode(firebaseJsonFileContent) as Map; + expect(iosResult.stdout, 'success'); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [kFlutter, kPlatforms, kIos, kDefaultConfig], - '$kIos/$defaultTarget/$appleServiceFileName', - ); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [ - kFlutter, - kPlatforms, + final macosXcodeProject = p.join( + projectPath!, kMacos, - kDefaultConfig, - ], - '$kMacos/$defaultTarget/$appleServiceFileName', + 'Runner.xcodeproj', + ); + + final scriptToCheckMacosPbxprojFile = + rubyScriptForTestingDefaultConfigure( + macosXcodeProject, + ); + + final macosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckMacosPbxprojFile, + ], + runInShell: true, + ); + + if (macosResult.exitCode != 0) { + fail(macosResult.stderr as String); + } + + expect(macosResult.stdout, 'success'); + } + + // check google-services.json was created and has correct content + final androidServiceFilePath = p.join( + projectPath!, + 'android', + 'app', + androidServiceFileName, ); + testAndroidServiceFileValues(androidServiceFilePath); + + // Check android "android/settings.gradle" & "android/app/build.gradle" were updated + await checkBuildGradleFileUpdated(projectPath!); + + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); - checkAndroidFirebaseJsonValues( - decodedFirebaseJson, + await testFirebaseOptionsFileValues(firebaseOptions); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); + + test( + 'flutterfire configure: android - "build configuration" Apple - "build configuration"', + () async { + final result = Process.runSync( + 'flutterfire', [ - kFlutter, - kPlatforms, - kAndroid, - kDefaultConfig, + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=android,ios,macos,web,windows', + '--web-app-id=$webAppId', + '--windows-app-id=$windowsAppId', + // Android just requires the `--android-out` flag to be set + '--android-out=android/app/$buildType', + // Apple required the `--ios-out` and `--macos-out` flags to be set & the build type, + // We're using `Debug` for both which is a standard build configuration for an apple Flutter app + '--ios-out=ios/$buildType', + '--ios-build-config=$appleBuildConfiguration', + '--macos-out=macos/$buildType', + '--macos-build-config=$appleBuildConfiguration', ], - 'android/app/$androidServiceFileName', + workingDirectory: projectPath, + runInShell: true, ); - const defaultFilePath = 'lib/firebase_options.dart'; - final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; + if (result.exitCode != 0) { + fail(result.stderr as String); + } - checkDartFirebaseJsonValues( - decodedFirebaseJson, - keysToMapDart, - ); + if (Platform.isMacOS) { + // check Apple service files were created and have correct content + final iosPath = p.join( + projectPath!, + kIos, + buildType, + appleServiceFileName, + ); + final macosPath = p.join( + projectPath!, + kMacos, + buildType, + ); + + await testAppleServiceFileValues(iosPath); + await testAppleServiceFileValues( + macosPath, + platform: kMacos, + ); + + // check default "firebase.json" was created and has correct content + final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); + final firebaseJsonFileContent = + await File(firebaseJsonFile).readAsString(); + + final decodedFirebaseJson = + jsonDecode(firebaseJsonFileContent) as Map; + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kIos, + kBuildConfiguration, + appleBuildConfiguration, + ], + 'ios/$buildType/GoogleService-Info.plist', + ); + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kMacos, + kBuildConfiguration, + appleBuildConfiguration, + ], + 'macos/$buildType/GoogleService-Info.plist', + ); + + checkAndroidFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kAndroid, + kBuildConfiguration, + buildType, + ], + 'android/app/$buildType/google-services.json', + ); + + const defaultFilePath = 'lib/firebase_options.dart'; + final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; + checkDartFirebaseJsonValues( + decodedFirebaseJson, + keysToMapDart, + ); + + final scriptToCheckIosPbxprojFile = + rubyScriptForCheckingBundleResourcesScript( + projectPath!, + kIos, + ); + + final iosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckIosPbxprojFile, + ], + runInShell: true, + ); + + if (iosResult.exitCode != 0) { + fail(iosResult.stderr as String); + } + + expect(iosResult.stdout, 'success'); + + final scriptToCheckMacosPbxprojFile = + rubyScriptForCheckingBundleResourcesScript( + projectPath!, + kMacos, + ); + + final macosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckMacosPbxprojFile, + ], + runInShell: true, + ); + + if (macosResult.exitCode != 0) { + fail(macosResult.stderr as String); + } + + expect(macosResult.stdout, 'success'); + } - // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files - final iosXcodeProject = p.join( + // check google-services.json was created and has correct content + final androidServiceFilePath = p.join( projectPath!, - kIos, - 'Runner.xcodeproj', + 'android', + 'app', + buildType, + 'google-services.json', ); + testAndroidServiceFileValues(androidServiceFilePath); + + // Check android "android/settings.gradle" & "android/app/build.gradle" were updated + await checkBuildGradleFileUpdated(projectPath!); - final scriptToCheckIosPbxprojFile = - rubyScriptForTestingDefaultConfigure(iosXcodeProject); + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); - final iosResult = Process.runSync( - 'ruby', + await testFirebaseOptionsFileValues(firebaseOptions); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); + + test( + 'flutterfire configure: android - "default" Apple - "target"', + () async { + const targetType = 'Runner'; + const applePath = 'staging/target'; + const androidBuildConfiguration = 'development'; + final result = Process.runSync( + 'flutterfire', [ - '-e', - scriptToCheckIosPbxprojFile, + 'configure', + '--yes', + '--project=$firebaseProjectId', + // The below args needed for CI + '--platforms=android,ios,macos,web,windows', + '--web-app-id=$webAppId', + '--windows-app-id=$windowsAppId', + // Android just requires the `--android-out` flag to be set + '--android-out=android/app/$androidBuildConfiguration', + // Apple required the `--ios-out` and `--macos-out` flags to be set & the build type, + // We're using `Runner` target for both which is the standard target for an apple Flutter app + '--ios-out=ios/$applePath', + '--ios-target=$targetType', + '--macos-out=macos/$applePath', + '--macos-target=$targetType', ], + workingDirectory: projectPath, runInShell: true, ); - if (iosResult.exitCode != 0) { - fail(iosResult.stderr as String); + if (result.exitCode != 0) { + fail(result.stderr as String); } - expect(iosResult.stdout, 'success'); + if (Platform.isMacOS) { + // check Apple service files were created and have correct content + final iosPath = + p.join(projectPath!, kIos, applePath, appleServiceFileName); + final macosPath = p.join(projectPath!, kMacos, applePath); + + await testAppleServiceFileValues(iosPath); + await testAppleServiceFileValues( + macosPath, + platform: kMacos, + ); + + // check default "firebase.json" was created and has correct content + final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); + final firebaseJsonFileContent = + await File(firebaseJsonFile).readAsString(); + + final decodedFirebaseJson = + jsonDecode(firebaseJsonFileContent) as Map; + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kIos, + kTargets, + targetType, + ], + 'ios/$applePath/GoogleService-Info.plist', + ); + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [kFlutter, kPlatforms, kMacos, kTargets, targetType], + 'macos/$applePath/GoogleService-Info.plist', + ); + + checkAndroidFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kAndroid, + kBuildConfiguration, + androidBuildConfiguration, + ], + 'android/app/$androidBuildConfiguration/google-services.json', + ); + + // Check dart map is correct + const defaultFilePath = 'lib/firebase_options.dart'; + final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; + checkDartFirebaseJsonValues(decodedFirebaseJson, keysToMapDart); + + // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files + final iosXcodeProject = p.join( + projectPath!, + kIos, + 'Runner.xcodeproj', + ); + + final scriptToCheckIosPbxprojFile = + rubyScriptForTestingDefaultConfigure(iosXcodeProject); + + final iosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckIosPbxprojFile, + ], + runInShell: true, + ); + + if (iosResult.exitCode != 0) { + fail(iosResult.stderr as String); + } - final macosXcodeProject = p.join( + expect(iosResult.stdout, 'success'); + + final macosXcodeProject = p.join( + projectPath!, + kMacos, + 'Runner.xcodeproj', + ); + + final scriptToCheckMacosPbxprojFile = + rubyScriptForTestingDefaultConfigure( + macosXcodeProject, + ); + + final macosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckMacosPbxprojFile, + ], + runInShell: true, + ); + + if (macosResult.exitCode != 0) { + fail(macosResult.stderr as String); + } + + expect(macosResult.stdout, 'success'); + } + + // check google-services.json was created and has correct content + final androidServiceFilePath = p.join( projectPath!, - kMacos, - 'Runner.xcodeproj', + 'android', + 'app', + androidBuildConfiguration, + 'google-services.json', ); + testAndroidServiceFileValues(androidServiceFilePath); - final scriptToCheckMacosPbxprojFile = - rubyScriptForTestingDefaultConfigure( - macosXcodeProject, - ); + // Check android "android/settings.gradle" & "android/app/build.gradle" were updated + await checkBuildGradleFileUpdated(projectPath!); + + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); - final macosResult = Process.runSync( - 'ruby', + await testFirebaseOptionsFileValues(firebaseOptions); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); + test( + 'flutterfire configure: rewrite service files when rerunning "flutterfire configure" with different apps', + () async { + const defaultTarget = 'Runner'; + // The initial configuration + final result = Process.runSync( + 'flutterfire', [ - '-e', - scriptToCheckMacosPbxprojFile, + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=android,ios,macos,web', + '--web-app-id=$webAppId', + '--windows-app-id=$windowsAppId', ], + workingDirectory: projectPath, runInShell: true, ); - if (macosResult.exitCode != 0) { - fail(macosResult.stderr as String); + if (result.exitCode != 0) { + fail(result.stderr as String); } - expect(macosResult.stdout, 'success'); - } + // The second configuration with different bundle ids which we need to check + final result2 = Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=android,ios,macos,web,windows', + '--ios-bundle-id=com.example.secondApp', + '--android-package-name=com.example.second_app', + '--macos-bundle-id=com.example.secondApp', + '--web-app-id=$secondWebAppId', + '--windows-app-id=$secondWindowsAppId', + ], + workingDirectory: projectPath, + runInShell: true, + ); - // check google-services.json was created and has correct content - final androidServiceFilePath = p.join( - projectPath!, - 'android', - 'app', - androidServiceFileName, - ); - testAndroidServiceFileValues(androidServiceFilePath); + if (result2.exitCode != 0) { + fail(result2.stderr as String); + } - // Check android "android/settings.gradle" & "android/app/build.gradle" were updated - await checkBuildGradleFileUpdated(projectPath!); + if (Platform.isMacOS) { + // check Apple service files were created and have correct content + final iosPath = + p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); + final macosPath = p.join(projectPath!, kMacos, defaultTarget); + + await testAppleServiceFileValues( + iosPath, + appId: secondAppleAppId, + bundleId: secondAppleBundleId, + ); + await testAppleServiceFileValues( + macosPath, + platform: kMacos, + appId: secondAppleAppId, + bundleId: secondAppleBundleId, + ); + + // check default "firebase.json" was created and has correct content + final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); + final firebaseJsonFileContent = + await File(firebaseJsonFile).readAsString(); + + final decodedFirebaseJson = + jsonDecode(firebaseJsonFileContent) as Map; + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [kFlutter, kPlatforms, kIos, kDefaultConfig], + '$kIos/$defaultTarget/$appleServiceFileName', + appId: secondAppleAppId, + ); + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kMacos, + kDefaultConfig, + ], + '$kMacos/$defaultTarget/$appleServiceFileName', + appId: secondAppleAppId, + ); + + checkAndroidFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kAndroid, + kDefaultConfig, + ], + 'android/app/$androidServiceFileName', + appId: secondAndroidAppId, + ); + + const defaultFilePath = 'lib/firebase_options.dart'; + final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; + + checkDartFirebaseJsonValues( + decodedFirebaseJson, + keysToMapDart, + androidAppId: secondAndroidAppId, + appleAppId: secondAppleAppId, + webAppId: secondWebAppId, + ); + + // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files + final iosXcodeProject = p.join( + projectPath!, + kIos, + 'Runner.xcodeproj', + ); - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); + final scriptToCheckIosPbxprojFile = + rubyScriptForTestingDefaultConfigure(iosXcodeProject); - await testFirebaseOptionsFileValues(firebaseOptions); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); + final iosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckIosPbxprojFile, + ], + ); - test( - 'flutterfire configure: android - "build configuration" Apple - "build configuration"', - () async { - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=android,ios,macos,web,windows', - '--web-app-id=$webAppId', - '--windows-app-id=$windowsAppId', - // Android just requires the `--android-out` flag to be set - '--android-out=android/app/$buildType', - // Apple required the `--ios-out` and `--macos-out` flags to be set & the build type, - // We're using `Debug` for both which is a standard build configuration for an apple Flutter app - '--ios-out=ios/$buildType', - '--ios-build-config=$appleBuildConfiguration', - '--macos-out=macos/$buildType', - '--macos-build-config=$appleBuildConfiguration', - ], - workingDirectory: projectPath, - runInShell: true, - ); + if (iosResult.exitCode != 0) { + fail(iosResult.stderr as String); + } - if (result.exitCode != 0) { - fail(result.stderr as String); - } + expect(iosResult.stdout, 'success'); + + final macosXcodeProject = p.join( + projectPath!, + kMacos, + 'Runner.xcodeproj', + ); + + final scriptToCheckMacosPbxprojFile = + rubyScriptForTestingDefaultConfigure( + macosXcodeProject, + ); + + final macosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckMacosPbxprojFile, + ], + ); + + if (macosResult.exitCode != 0) { + fail(macosResult.stderr as String); + } + + expect(macosResult.stdout, 'success'); + } - if (Platform.isMacOS) { - // check Apple service files were created and have correct content - final iosPath = p.join( + // check google-services.json was created and has correct content + final androidServiceFilePath = p.join( projectPath!, - kIos, - buildType, - appleServiceFileName, + 'android', + 'app', + androidServiceFileName, ); - final macosPath = p.join( - projectPath!, - kMacos, - buildType, + testAndroidServiceFileValues( + androidServiceFilePath, + appId: secondAndroidAppId, ); - await testAppleServiceFileValues(iosPath); - await testAppleServiceFileValues( - macosPath, - platform: kMacos, + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); + + final firebaseOptionsContent = + await File(firebaseOptions).readAsString(); + + expect( + firebaseOptionsContent.split('\n'), + containsAll([ + contains(secondAppleAppId), + contains(secondAppleBundleId), + contains(secondAndroidAppId), + contains(secondWebAppId), + contains(secondWindowsAppId), + contains('static const FirebaseOptions web = FirebaseOptions'), + contains('static const FirebaseOptions android = FirebaseOptions'), + contains('static const FirebaseOptions ios = FirebaseOptions'), + contains('static const FirebaseOptions macos = FirebaseOptions'), + contains('static const FirebaseOptions windows = FirebaseOptions'), + ]), ); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); - // check default "firebase.json" was created and has correct content - final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); - final firebaseJsonFileContent = - await File(firebaseJsonFile).readAsString(); - - final decodedFirebaseJson = - jsonDecode(firebaseJsonFileContent) as Map; + test( + 'flutterfire configure: test when only two platforms are selected, not including "web" platform', + () async { + const defaultTarget = 'Runner'; - checkAppleFirebaseJsonValues( - decodedFirebaseJson, + final result = Process.runSync( + 'flutterfire', [ - kFlutter, - kPlatforms, - kIos, - kBuildConfiguration, - appleBuildConfiguration, + 'configure', + '--yes', + '--platforms=ios,android', + '--project=$firebaseProjectId', ], - 'ios/$buildType/GoogleService-Info.plist', + workingDirectory: projectPath, + runInShell: true, ); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [ - kFlutter, - kPlatforms, - kMacos, - kBuildConfiguration, - appleBuildConfiguration, - ], - 'macos/$buildType/GoogleService-Info.plist', + if (result.exitCode != 0) { + fail(result.stderr as String); + } + + if (Platform.isMacOS) { + // check iOS service files were created and have correct content + final iosPath = + p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); + + await testAppleServiceFileValues( + iosPath, + ); + + // check default "firebase.json" was created and has correct content + final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); + final firebaseJsonFileContent = + await File(firebaseJsonFile).readAsString(); + + final decodedFirebaseJson = + jsonDecode(firebaseJsonFileContent) as Map; + + checkAppleFirebaseJsonValues( + decodedFirebaseJson, + [kFlutter, kPlatforms, kIos, kDefaultConfig], + '$kIos/$defaultTarget/$appleServiceFileName', + ); + + checkAndroidFirebaseJsonValues( + decodedFirebaseJson, + [ + kFlutter, + kPlatforms, + kAndroid, + kDefaultConfig, + ], + 'android/app/$androidServiceFileName', + ); + + const defaultFilePath = 'lib/firebase_options.dart'; + final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; + + checkDartFirebaseJsonValues( + decodedFirebaseJson, + keysToMapDart, + checkMacos: false, + checkWeb: false, + ); + + // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files + final iosXcodeProject = p.join( + projectPath!, + kIos, + 'Runner.xcodeproj', + ); + + final scriptToCheckIosPbxprojFile = + rubyScriptForTestingDefaultConfigure(iosXcodeProject); + + final iosResult = Process.runSync( + 'ruby', + [ + '-e', + scriptToCheckIosPbxprojFile, + ], + ); + + if (iosResult.exitCode != 0) { + fail(iosResult.stderr as String); + } + + expect(iosResult.stdout, 'success'); + } + + // check google-services.json was created and has correct content + final androidServiceFilePath = p.join( + projectPath!, + 'android', + 'app', + androidServiceFileName, + ); + testAndroidServiceFileValues( + androidServiceFilePath, + ); + + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); + + final firebaseOptionsContent = + await File(firebaseOptions).readAsString(); + + final listOfStrings = firebaseOptionsContent.split('\n'); + expect( + listOfStrings, + containsAll([ + contains(appleAppId), + contains(appleBundleId), + contains(androidAppId), + contains('static const FirebaseOptions android = FirebaseOptions'), + contains('static const FirebaseOptions ios = FirebaseOptions'), + ]), + ); + expect( + firebaseOptionsContent + .contains('static const FirebaseOptions web = FirebaseOptions'), + isFalse, ); + expect( + firebaseOptionsContent.contains( + 'static const FirebaseOptions windows = FirebaseOptions', + ), + isFalse, + ); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); - checkAndroidFirebaseJsonValues( - decodedFirebaseJson, + test( + 'flutterfire configure: test will reconfigure project if no args and `firebase.json` is present', + () async { + const defaultTarget = 'Runner'; + // Set up initial configuration + final result = Process.runSync( + 'flutterfire', [ - kFlutter, - kPlatforms, - kAndroid, - kBuildConfiguration, - buildType, + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=android,ios', ], - 'android/app/$buildType/google-services.json', + workingDirectory: projectPath, + runInShell: true, ); - const defaultFilePath = 'lib/firebase_options.dart'; - final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; - checkDartFirebaseJsonValues( - decodedFirebaseJson, - keysToMapDart, - ); + if (result.exitCode != 0) { + fail(result.stderr as String); + } - final scriptToCheckIosPbxprojFile = - rubyScriptForCheckingBundleResourcesScript( + // Now firebase.json file has been written, change values in service files to test they are rewritten + if (Platform.isMacOS) { + final iosPath = + p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); + + // Clean out file to test it was recreated + await File(iosPath).writeAsString(''); + } + + final androidServiceFilePath = p.join( projectPath!, - kIos, + 'android', + 'app', + androidServiceFileName, ); + // Clean out file to test it was recreated + await File(androidServiceFilePath).writeAsString(''); - final iosResult = Process.runSync( - 'ruby', + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); + + final accessToken = await generateAccessTokenCI(); + // Perform `flutterfire configure` without args to use `flutterfire reconfigure`. + final result2 = Process.runSync( + 'flutterfire', [ - '-e', - scriptToCheckIosPbxprojFile, + 'configure', + if (accessToken != null) '--test-access-token=$accessToken', ], + workingDirectory: projectPath, runInShell: true, + environment: {'TEST_ENVIRONMENT': 'true'}, ); - if (iosResult.exitCode != 0) { - fail(iosResult.stderr as String); + if (result2.exitCode != 0) { + fail(result2.stderr as String); } - expect(iosResult.stdout, 'success'); + if (Platform.isMacOS) { + // check iOS service file was recreated and has correct content + final iosPath = + p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); - final scriptToCheckMacosPbxprojFile = - rubyScriptForCheckingBundleResourcesScript( - projectPath!, - kMacos, + await testAppleServiceFileValues( + iosPath, + ); + } + + // check google-services.json was recreated and has correct content + testAndroidServiceFileValues( + androidServiceFilePath, ); - final macosResult = Process.runSync( - 'ruby', + // check "firebase_options.dart" file was recreated in lib directory + final firebaseOptionsContent = + await File(firebaseOptions).readAsString(); + + final listOfStrings = firebaseOptionsContent.split('\n'); + expect( + listOfStrings, + containsAll([ + contains(appleAppId), + contains(appleBundleId), + contains(androidAppId), + contains('static const FirebaseOptions android = FirebaseOptions'), + contains('static const FirebaseOptions ios = FirebaseOptions'), + ]), + ); + expect( + firebaseOptionsContent + .contains('static const FirebaseOptions web = FirebaseOptions'), + isFalse, + ); + expect( + firebaseOptionsContent.contains( + 'static const FirebaseOptions windows = FirebaseOptions', + ), + isFalse, + ); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); + + test( + 'flutterfire configure: write Dart configuration file to different output', + () async { + const configurationFileName = 'different_firebase_options.dart'; + // Set up initial configuration + final result = Process.runSync( + 'flutterfire', [ - '-e', - scriptToCheckMacosPbxprojFile, + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=android,ios', + // The below args aren't needed unless running from CI. We need for Github actions to run command. + '--platforms=android,ios,macos,web,windows', + '--web-app-id=$webAppId', + '--windows-app-id=$windowsAppId', + // Output to different file + '--out=lib/$configurationFileName', ], + workingDirectory: projectPath, runInShell: true, ); - if (macosResult.exitCode != 0) { - fail(macosResult.stderr as String); + if (result.exitCode != 0) { + fail(result.stderr as String); } - expect(macosResult.stdout, 'success'); - } + final firebaseOptions = + p.join(projectPath!, 'lib', configurationFileName); + + // check "different_firebase_options.dart" file was recreated in lib directory + final firebaseOptionsContent = + await File(firebaseOptions).readAsString(); + + final listOfStrings = firebaseOptionsContent.split('\n'); + expect( + listOfStrings, + containsAll([ + contains(appleAppId), + contains(appleBundleId), + contains(androidAppId), + contains(webAppId), + contains(windowsAppId), + contains('static const FirebaseOptions android = FirebaseOptions'), + contains('static const FirebaseOptions ios = FirebaseOptions'), + contains('static const FirebaseOptions web = FirebaseOptions'), + contains('static const FirebaseOptions windows = FirebaseOptions'), + ]), + ); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); - // check google-services.json was created and has correct content - final androidServiceFilePath = p.join( - projectPath!, - 'android', - 'app', - buildType, - 'google-services.json', + test( + 'flutterfire configure: incorrect `--web-app-id` should throw exception', + () async { + final result = Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=web', + '--web-app-id=a-non-existent-web-app-id', + ], + workingDirectory: projectPath, + runInShell: true, ); - testAndroidServiceFileValues(androidServiceFilePath); - // Check android "android/settings.gradle" & "android/app/build.gradle" were updated - await checkBuildGradleFileUpdated(projectPath!); + final output = result.stderr as String; + final expectedOutput = [ + 'does not match the web app id of any existing Firebase app', + 'Exception', + ]; - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); + final expected = expectedOutput.every(output.contains); + expect(result.exitCode != 0, isTrue); + expect( + expected, + isTrue, + ); + }); - await testFirebaseOptionsFileValues(firebaseOptions); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test( - 'flutterfire configure: android - "default" Apple - "target"', - () async { - const targetType = 'Runner'; - const applePath = 'staging/target'; - const androidBuildConfiguration = 'development'; + test( + 'flutterfire configure: incorrect `--windows-app-id` should throw exception', + () async { final result = Process.runSync( 'flutterfire', [ 'configure', '--yes', '--project=$firebaseProjectId', - // The below args needed for CI - '--platforms=android,ios,macos,web,windows', - '--web-app-id=$webAppId', - '--windows-app-id=$windowsAppId', - // Android just requires the `--android-out` flag to be set - '--android-out=android/app/$androidBuildConfiguration', - // Apple required the `--ios-out` and `--macos-out` flags to be set & the build type, - // We're using `Runner` target for both which is the standard target for an apple Flutter app - '--ios-out=ios/$applePath', - '--ios-target=$targetType', - '--macos-out=macos/$applePath', - '--macos-target=$targetType', + '--platforms=windows', + // Trigger the exception + '--windows-app-id=a-non-existent-windows-app-id', ], workingDirectory: projectPath, runInShell: true, ); - if (result.exitCode != 0) { - fail(result.stderr as String); - } + final output = result.stderr as String; + final expectedOutput = [ + 'does not match the web app id of any existing Firebase app', + 'Exception', + ]; - if (Platform.isMacOS) { - // check Apple service files were created and have correct content - final iosPath = - p.join(projectPath!, kIos, applePath, appleServiceFileName); - final macosPath = p.join(projectPath!, kMacos, applePath); + final expected = expectedOutput.every(output.contains); - await testAppleServiceFileValues(iosPath); - await testAppleServiceFileValues( - macosPath, - platform: kMacos, - ); + expect(result.exitCode != 0, isTrue); + expect( + expected, + isTrue, + ); + }); + + test( + 'flutterfire configure: get correct Firebase App with manually created Firebase web app via `--web-app-id`', + () async { + final result = Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=web', + '--web-app-id=$secondWebAppId', + ], + workingDirectory: projectPath, + runInShell: true, + ); - // check default "firebase.json" was created and has correct content - final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); - final firebaseJsonFileContent = - await File(firebaseJsonFile).readAsString(); + expect(result.exitCode, 0); + // Console out put looks like this on success: "web 1:262904632156:web:cb3a00412ed430ca2f2799" + expect( + (result.stdout as String).contains( + 'web $secondWebAppId', + ), + isTrue, + ); - final decodedFirebaseJson = - jsonDecode(firebaseJsonFileContent) as Map; + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [ - kFlutter, - kPlatforms, - kIos, - kTargets, - targetType, - ], - 'ios/$applePath/GoogleService-Info.plist', - ); + await testFirebaseOptionsFileValues( + firebaseOptions, + selectedPlatform: kWeb, + ); + }); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [kFlutter, kPlatforms, kMacos, kTargets, targetType], - 'macos/$applePath/GoogleService-Info.plist', - ); + test( + 'flutterfire configure: get correct Firebase App with manually created Firebase web app via `--windows-app-id`', + () async { + final result = Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=windows', + '--windows-app-id=$secondWindowsAppId', + ], + workingDirectory: projectPath, + runInShell: true, + ); + + expect(result.exitCode, 0); + + // check "firebase_options.dart" file is created in lib directory + final firebaseOptions = + p.join(projectPath!, 'lib', 'firebase_options.dart'); - checkAndroidFirebaseJsonValues( - decodedFirebaseJson, + await testFirebaseOptionsFileValues( + firebaseOptions, + selectedPlatform: kWindows, + ); + }); + + test( + 'flutterfire configure: check Dart file configuration is updated correctly', + () async { + // Set up initial configuration + final result = Process.runSync( + 'flutterfire', [ - kFlutter, - kPlatforms, - kAndroid, - kBuildConfiguration, - androidBuildConfiguration, + 'configure', + '--yes', + '--project=$firebaseProjectId', + // Only configuring android initially + '--platforms=android', ], - 'android/app/$androidBuildConfiguration/google-services.json', + workingDirectory: projectPath, + runInShell: true, ); - // Check dart map is correct - const defaultFilePath = 'lib/firebase_options.dart'; - final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; - checkDartFirebaseJsonValues(decodedFirebaseJson, keysToMapDart); + if (result.exitCode != 0) { + fail(result.stderr as String); + } - // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files - final iosXcodeProject = p.join( + final firebaseOptions = p.join( projectPath!, - kIos, - 'Runner.xcodeproj', + 'lib', + 'firebase_options.dart', ); - final scriptToCheckIosPbxprojFile = - rubyScriptForTestingDefaultConfigure(iosXcodeProject); + // check "firebase_options.dart" file was created and updates android only + final firebaseOptionsContent = + await File(firebaseOptions).readAsString(); + + // only configure android initially + final listOfStrings = firebaseOptionsContent.split('\n'); + expect( + listOfStrings, + containsAll([ + contains(androidAppId), + contains('static const FirebaseOptions android = FirebaseOptions'), + ]), + ); + expect( + firebaseOptionsContent + .contains('static const FirebaseOptions web = FirebaseOptions'), + isFalse, + ); + expect( + firebaseOptionsContent + .contains('static const FirebaseOptions ios = FirebaseOptions'), + isFalse, + ); - final iosResult = Process.runSync( - 'ruby', + // Now reconfigure with ios, macos & web platforms and check Dart file is updated correctly + final result2 = Process.runSync( + 'flutterfire', [ - '-e', - scriptToCheckIosPbxprojFile, + 'configure', + '--yes', + '--project=$firebaseProjectId', + // Configure the rest of the platforms + '--platforms=ios,macos,web', + '--web-app-id=$webAppId', ], + workingDirectory: projectPath, runInShell: true, ); - if (iosResult.exitCode != 0) { - fail(iosResult.stderr as String); + if (result2.exitCode != 0) { + fail(result.stderr as String); } - expect(iosResult.stdout, 'success'); + final firebaseOptionsContent2 = + await File(firebaseOptions).readAsString(); + final listOfStrings2 = firebaseOptionsContent2.split('\n'); + + expect( + listOfStrings2, + containsAll([ + contains(androidAppId), + contains('static const FirebaseOptions web = FirebaseOptions'), + contains('static const FirebaseOptions android = FirebaseOptions'), + contains('static const FirebaseOptions ios = FirebaseOptions'), + ]), + ); - final macosXcodeProject = p.join( - projectPath!, - kMacos, - 'Runner.xcodeproj', + final startIndexWeb = listOfStrings2.indexWhere( + (line) => line.contains( + 'if (kIsWeb)', + ), ); - final scriptToCheckMacosPbxprojFile = - rubyScriptForTestingDefaultConfigure( - macosXcodeProject, + listOfStrings2[startIndexWeb + 1].contains( + 'return web;', ); - final macosResult = Process.runSync( - 'ruby', - [ - '-e', - scriptToCheckMacosPbxprojFile, - ], - runInShell: true, + final startIndexAndroid = listOfStrings2.indexWhere( + (line) => line.contains( + 'case TargetPlatform.android:', + ), ); - if (macosResult.exitCode != 0) { - fail(macosResult.stderr as String); - } + listOfStrings2[startIndexAndroid + 1].contains( + 'return android;', + ); - expect(macosResult.stdout, 'success'); - } + final startIndexIos = listOfStrings2.indexWhere( + (line) => line.contains( + 'case TargetPlatform.iOS:', + ), + ); - // check google-services.json was created and has correct content - final androidServiceFilePath = p.join( - projectPath!, - 'android', - 'app', - androidBuildConfiguration, - 'google-services.json', - ); - testAndroidServiceFileValues(androidServiceFilePath); + listOfStrings2[startIndexIos + 1].contains( + 'return ios;', + ); - // Check android "android/settings.gradle" & "android/app/build.gradle" were updated - await checkBuildGradleFileUpdated(projectPath!); + final startIndexMacos = listOfStrings2.indexWhere( + (line) => line.contains( + 'case TargetPlatform.macOS:', + ), + ); - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); + listOfStrings2[startIndexMacos + 1].contains( + 'return macos;', + ); + + // Now reconfigure with different apps across platforms and check Dart file is updated correctly + final result3 = Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=ios,macos,web,android,windows', + '--ios-bundle-id=$secondAppleBundleId', + '--android-package-name=$secondAndroidApplicationId', + '--macos-bundle-id=$secondAppleBundleId', + '--web-app-id=$secondWebAppId', + '--windows-app-id=$secondWindowsAppId', + ], + workingDirectory: projectPath, + runInShell: true, + ); + + if (result3.exitCode != 0) { + fail(result.stderr as String); + } - await testFirebaseOptionsFileValues(firebaseOptions); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); + final firebaseOptionsContent3 = + await File(firebaseOptions).readAsString(); + final listOfStrings3 = firebaseOptionsContent3.split('\n'); + + expect( + listOfStrings3, + containsAll([ + contains(secondAndroidAppId), + contains(secondAppleAppId), + contains(secondWebAppId), + contains(secondWindowsAppId), + ]), + ); + }, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); - test( - 'Validate `flutterfire upload-crashlytics-symbols` script is ran when building app', - () async { + test( + 'flutterfire configure: ensure android build.gradle files are only updated once', + () async { + // Add crashlytics and performance to check they are only created once final result = Process.runSync( 'flutter', - ['pub', 'add', 'firebase_crashlytics'], + ['pub', 'add', 'firebase_crashlytics', 'firebase_performance'], workingDirectory: projectPath, ); if (result.exitCode != 0) { fail(result.stderr as String); } - + // Run first time to update + Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + // Only android + '--platforms=android', + ], + workingDirectory: projectPath, + runInShell: true, + ); + // Run second time and check it was only updated once final result2 = Process.runSync( 'flutterfire', [ 'configure', '--yes', '--project=$firebaseProjectId', - '--platforms=android,ios,macos,web,windows', - '--web-app-id=$webAppId', - '--windows-app-id=$windowsAppId', + // Only android + '--platforms=android', ], workingDirectory: projectPath, runInShell: true, @@ -533,1130 +1335,324 @@ void main() { fail(result2.stderr as String); } - const iosVersion = '13.0'; - // Update project.pbxproj - final pbxprojResult = Process.runSync( - 'sed', - [ - '-i', - '', - 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = $iosVersion;/', - 'ios/Runner.xcodeproj/project.pbxproj', - ], - workingDirectory: projectPath, - ); - - if (pbxprojResult.exitCode != 0) { - fail(pbxprojResult.stderr as String); - } - - final buildApp = Process.runSync( - 'flutter', - [ - 'build', - 'ios', - '--no-codesign', - '--simulator', - '--debug', - '--verbose', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - if (buildApp.exitCode != 0) { - fail(buildApp.stderr as String); - } - - expect( - buildApp.stdout, - // Check symbols are uploaded in the background - contains('Symbol uploading will proceed in the background'), - ); - - Process.runSync( - 'flutter', - ['config', '--enable-swift-package-manager'], - workingDirectory: projectPath, - ); - - final iosDirectory = p.join(projectPath!, 'ios'); - - Process.runSync( - 'bash', - [ - '-c', - '[ -f Podfile ] && rm Podfile && pod deintegrate && rm -rf Pods/', - ], - workingDirectory: iosDirectory, - ); - - final buildAppSPM = Process.runSync( - 'flutter', - [ - 'build', - 'ios', - '--no-codesign', - '--simulator', - '--debug', - '--verbose', - ], - workingDirectory: projectPath, - runInShell: true, + await checkBuildGradleFileUpdated( + projectPath!, + checkCrashlytics: true, + checkPerf: true, ); + }); - if (buildAppSPM.exitCode != 0) { - fail(buildAppSPM.stderr as String); - } - - expect( - buildAppSPM.stdout, - // Check symbols are uploaded in the background - contains('Symbol uploading will proceed in the background'), - ); - }, - skip: !Platform.isMacOS, - timeout: const Timeout( - Duration(minutes: 3), - ), - ); - - test( - 'flutterfire configure: rewrite service files when rerunning "flutterfire configure" with different apps', - () async { - const defaultTarget = 'Runner'; - // The initial configuration - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=android,ios,macos,web', - '--web-app-id=$webAppId', - '--windows-app-id=$windowsAppId', - ], - workingDirectory: projectPath, - runInShell: true, - ); + test( + 'flutterfire configure: path with spaces should not break the configuration for iOS/macOS', + () async { + // Regression test for https://github.com/invertase/flutterfire_cli/pull/228 + const targetType = 'Runner'; + const iOSPath = 'ios path with spaces/target'; + const macOSPath = 'macos path with spaces/target'; + final result = Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + '--project=$firebaseProjectId', + // The below args aren't needed unless running from CI. We need for Github actions to run command. + '--platforms=ios,macos', + // Apple required the `--ios-out` and `--macos-out` flags to be set & the build type, + // We're using `Runner` target for both which is the standard target for an apple Flutter app + '--ios-out=ios/$iOSPath', + '--ios-target=$targetType', + '--macos-out=macos/$macOSPath', + '--macos-target=$targetType', + ], + workingDirectory: projectPath, + runInShell: true, + ); - if (result.exitCode != 0) { - fail(result.stderr as String); - } + if (result.exitCode != 0) { + fail(result.stderr as String); + } - // The second configuration with different bundle ids which we need to check - final result2 = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=android,ios,macos,web,windows', - '--ios-bundle-id=com.example.secondApp', - '--android-package-name=com.example.second_app', - '--macos-bundle-id=com.example.secondApp', - '--web-app-id=$secondWebAppId', - '--windows-app-id=$secondWindowsAppId', - ], - workingDirectory: projectPath, - runInShell: true, - ); + if (Platform.isMacOS) { + // check Apple service files were created and have correct content + final iosPath = + p.join(projectPath!, kIos, iOSPath, appleServiceFileName); + final macosPath = p.join(projectPath!, kMacos, macOSPath); + + await testAppleServiceFileValues(iosPath); + await testAppleServiceFileValues( + macosPath, + platform: kMacos, + ); + } + }, + skip: !Platform.isMacOS, + timeout: const Timeout( + Duration(minutes: 2), + ), + ); + }); - if (result2.exitCode != 0) { - fail(result2.stderr as String); - } + // Separate group for tests that need iOS 15.0 + group('FlutterFire configure tests - build target (iOS 15.0)', () { + String? projectPath; + setUp(() async { + projectPath = await createFlutterProject(updateiOSBuildTarget: true); + }); - if (Platform.isMacOS) { - // check Apple service files were created and have correct content - final iosPath = - p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); - final macosPath = p.join(projectPath!, kMacos, defaultTarget); + tearDown(() { + Directory(p.dirname(projectPath!)).delete(recursive: true); + }); - await testAppleServiceFileValues( - iosPath, - appId: secondAppleAppId, - bundleId: secondAppleBundleId, - ); - await testAppleServiceFileValues( - macosPath, - platform: kMacos, - appId: secondAppleAppId, - bundleId: secondAppleBundleId, + test( + 'Validate dev dependency works as normal & `flutterfire upload-crashlytics-symbols` works via dev dependency', + () async { + final result = Process.runSync( + 'flutter', + ['pub', 'add', 'firebase_crashlytics'], + workingDirectory: projectPath, ); - // check default "firebase.json" was created and has correct content - final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); - final firebaseJsonFileContent = - await File(firebaseJsonFile).readAsString(); - - final decodedFirebaseJson = - jsonDecode(firebaseJsonFileContent) as Map; + if (result.exitCode != 0) { + fail(result.stderr as String); + } - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [kFlutter, kPlatforms, kIos, kDefaultConfig], - '$kIos/$defaultTarget/$appleServiceFileName', - appId: secondAppleAppId, - ); - checkAppleFirebaseJsonValues( - decodedFirebaseJson, + final installDevDependency = Process.runSync( + 'flutter', [ - kFlutter, - kPlatforms, - kMacos, - kDefaultConfig, + 'pub', + 'add', + '--dev', + 'flutterfire_cli', + '--path=${Directory.current.path}', ], - '$kMacos/$defaultTarget/$appleServiceFileName', - appId: secondAppleAppId, + workingDirectory: projectPath, ); - checkAndroidFirebaseJsonValues( - decodedFirebaseJson, + if (installDevDependency.exitCode != 0) { + fail(installDevDependency.stderr as String); + } + + final result2 = Process.runSync( + 'dart', [ - kFlutter, - kPlatforms, - kAndroid, - kDefaultConfig, + 'run', + 'flutterfire_cli:flutterfire', + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=ios,macos', + // Need to test bundle script is written correctly for both build configurations + '--ios-build-config=$appleBuildConfiguration', + '--ios-out=ios/$buildType', + '--macos-build-config=$appleBuildConfiguration', + '--macos-out=macos/$buildType', ], - 'android/app/$androidServiceFileName', - appId: secondAndroidAppId, - ); - - const defaultFilePath = 'lib/firebase_options.dart'; - final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; - - checkDartFirebaseJsonValues( - decodedFirebaseJson, - keysToMapDart, - androidAppId: secondAndroidAppId, - appleAppId: secondAppleAppId, - webAppId: secondWebAppId, - ); - - // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files - final iosXcodeProject = p.join( - projectPath!, - kIos, - 'Runner.xcodeproj', + workingDirectory: projectPath, + runInShell: true, ); - final scriptToCheckIosPbxprojFile = - rubyScriptForTestingDefaultConfigure(iosXcodeProject); + if (result2.exitCode != 0) { + fail(result2.stderr as String); + } - final iosResult = Process.runSync( - 'ruby', + // Run grep to check for both strings + final grepDevDependencyScriptsAdded = Process.runSync( + 'grep', [ - '-e', - scriptToCheckIosPbxprojFile, + '-q', + '-E', + 'dart run flutterfire_cli:flutterfire (upload-crashlytics-symbols|bundle-service-file)', + 'ios/Runner.xcodeproj/project.pbxproj', ], + workingDirectory: projectPath, ); - if (iosResult.exitCode != 0) { - fail(iosResult.stderr as String); - } - - expect(iosResult.stdout, 'success'); - - final macosXcodeProject = p.join( - projectPath!, - kMacos, - 'Runner.xcodeproj', + // Exit code 0 means both strings were found + expect( + grepDevDependencyScriptsAdded.exitCode, + 0, + reason: 'Required FlutterFire scripts not found in project.pbxproj', ); - final scriptToCheckMacosPbxprojFile = - rubyScriptForTestingDefaultConfigure( - macosXcodeProject, - ); + final buildArgs = [ + 'build', + 'ios', + '--no-codesign', + '--simulator', + '--debug', + '--verbose', + ]; + buildArgs.add('--device-id=98641507-FA54-4F7C-BBE9-4970C7C0EB55'); - final macosResult = Process.runSync( - 'ruby', - [ - '-e', - scriptToCheckMacosPbxprojFile, - ], + final buildApp = Process.runSync( + 'flutter', + buildArgs, + workingDirectory: projectPath, + runInShell: true, ); - if (macosResult.exitCode != 0) { - fail(macosResult.stderr as String); + if (buildApp.exitCode != 0) { + fail(buildApp.stderr as String); } - expect(macosResult.stdout, 'success'); - } - - // check google-services.json was created and has correct content - final androidServiceFilePath = p.join( - projectPath!, - 'android', - 'app', - androidServiceFileName, - ); - testAndroidServiceFileValues( - androidServiceFilePath, - appId: secondAndroidAppId, - ); - - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); - - final firebaseOptionsContent = await File(firebaseOptions).readAsString(); - - expect( - firebaseOptionsContent.split('\n'), - containsAll([ - contains(secondAppleAppId), - contains(secondAppleBundleId), - contains(secondAndroidAppId), - contains(secondWebAppId), - contains(secondWindowsAppId), - contains('static const FirebaseOptions web = FirebaseOptions'), - contains('static const FirebaseOptions android = FirebaseOptions'), - contains('static const FirebaseOptions ios = FirebaseOptions'), - contains('static const FirebaseOptions macos = FirebaseOptions'), - contains('static const FirebaseOptions windows = FirebaseOptions'), - ]), - ); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test( - 'flutterfire configure: test when only two platforms are selected, not including "web" platform', - () async { - const defaultTarget = 'Runner'; - - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--platforms=ios,android', - '--project=$firebaseProjectId', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - if (result.exitCode != 0) { - fail(result.stderr as String); - } - - if (Platform.isMacOS) { - // check iOS service files were created and have correct content - final iosPath = - p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); - - await testAppleServiceFileValues( - iosPath, + expect( + buildApp.stdout, + // Check symbols are uploaded in the background + contains('Symbol uploading will proceed in the background'), ); - // check default "firebase.json" was created and has correct content - final firebaseJsonFile = p.join(projectPath!, 'firebase.json'); - final firebaseJsonFileContent = - await File(firebaseJsonFile).readAsString(); - - final decodedFirebaseJson = - jsonDecode(firebaseJsonFileContent) as Map; - - checkAppleFirebaseJsonValues( - decodedFirebaseJson, - [kFlutter, kPlatforms, kIos, kDefaultConfig], - '$kIos/$defaultTarget/$appleServiceFileName', + Process.runSync( + 'flutter', + ['config', '--enable-swift-package-manager'], + workingDirectory: projectPath, ); - checkAndroidFirebaseJsonValues( - decodedFirebaseJson, + final iosDirectory = p.join(projectPath!, 'ios'); + + Process.runSync( + 'bash', [ - kFlutter, - kPlatforms, - kAndroid, - kDefaultConfig, + '-c', + '[ -f Podfile ] && rm Podfile && pod deintegrate && rm -rf Pods/', ], - 'android/app/$androidServiceFileName', - ); - - const defaultFilePath = 'lib/firebase_options.dart'; - final keysToMapDart = [kFlutter, kPlatforms, kDart, defaultFilePath]; - - checkDartFirebaseJsonValues( - decodedFirebaseJson, - keysToMapDart, - checkMacos: false, - checkWeb: false, + workingDirectory: iosDirectory, ); - // check GoogleService-Info.plist file is included & debug symbols script (until firebase crashlytics is a dependency) is not included in Apple "project.pbxproj" files - final iosXcodeProject = p.join( - projectPath!, - kIos, - 'Runner.xcodeproj', - ); - - final scriptToCheckIosPbxprojFile = - rubyScriptForTestingDefaultConfigure(iosXcodeProject); - - final iosResult = Process.runSync( - 'ruby', + final buildAppSPM = Process.runSync( + 'flutter', [ - '-e', - scriptToCheckIosPbxprojFile, + 'build', + 'ios', + '--no-codesign', + '--simulator', + '--debug', + '--verbose', ], + workingDirectory: projectPath, + runInShell: true, ); - if (iosResult.exitCode != 0) { - fail(iosResult.stderr as String); + if (buildAppSPM.exitCode != 0) { + fail(buildAppSPM.stderr as String); } - expect(iosResult.stdout, 'success'); - } - - // check google-services.json was created and has correct content - final androidServiceFilePath = p.join( - projectPath!, - 'android', - 'app', - androidServiceFileName, - ); - testAndroidServiceFileValues( - androidServiceFilePath, - ); - - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); - - final firebaseOptionsContent = await File(firebaseOptions).readAsString(); - - final listOfStrings = firebaseOptionsContent.split('\n'); - expect( - listOfStrings, - containsAll([ - contains(appleAppId), - contains(appleBundleId), - contains(androidAppId), - contains('static const FirebaseOptions android = FirebaseOptions'), - contains('static const FirebaseOptions ios = FirebaseOptions'), - ]), - ); - expect( - firebaseOptionsContent - .contains('static const FirebaseOptions web = FirebaseOptions'), - isFalse, - ); - expect( - firebaseOptionsContent - .contains('static const FirebaseOptions windows = FirebaseOptions'), - isFalse, - ); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test( - 'flutterfire configure: test will reconfigure project if no args and `firebase.json` is present', - () async { - const defaultTarget = 'Runner'; - // Set up initial configuration - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=android,ios', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - if (result.exitCode != 0) { - fail(result.stderr as String); - } - - // Now firebase.json file has been written, change values in service files to test they are rewritten - if (Platform.isMacOS) { - final iosPath = - p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); - - // Clean out file to test it was recreated - await File(iosPath).writeAsString(''); - } - - final androidServiceFilePath = p.join( - projectPath!, - 'android', - 'app', - androidServiceFileName, - ); - // Clean out file to test it was recreated - await File(androidServiceFilePath).writeAsString(''); - - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); - - final accessToken = await generateAccessTokenCI(); - // Perform `flutterfire configure` without args to use `flutterfire reconfigure`. - final result2 = Process.runSync( - 'flutterfire', - [ - 'configure', - if (accessToken != null) '--test-access-token=$accessToken', - ], - workingDirectory: projectPath, - runInShell: true, - environment: {'TEST_ENVIRONMENT': 'true'}, - ); - - if (result2.exitCode != 0) { - fail(result2.stderr as String); - } - - if (Platform.isMacOS) { - // check iOS service file was recreated and has correct content - final iosPath = - p.join(projectPath!, kIos, defaultTarget, appleServiceFileName); - - await testAppleServiceFileValues( - iosPath, + expect( + buildAppSPM.stdout, + // Check symbols are uploaded in the background + contains('Symbol uploading will proceed in the background'), ); - } - - // check google-services.json was recreated and has correct content - testAndroidServiceFileValues( - androidServiceFilePath, - ); - - // check "firebase_options.dart" file was recreated in lib directory - final firebaseOptionsContent = await File(firebaseOptions).readAsString(); - - final listOfStrings = firebaseOptionsContent.split('\n'); - expect( - listOfStrings, - containsAll([ - contains(appleAppId), - contains(appleBundleId), - contains(androidAppId), - contains('static const FirebaseOptions android = FirebaseOptions'), - contains('static const FirebaseOptions ios = FirebaseOptions'), - ]), - ); - expect( - firebaseOptionsContent - .contains('static const FirebaseOptions web = FirebaseOptions'), - isFalse, - ); - expect( - firebaseOptionsContent - .contains('static const FirebaseOptions windows = FirebaseOptions'), - isFalse, - ); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test( - 'flutterfire configure: write Dart configuration file to different output', - () async { - const configurationFileName = 'different_firebase_options.dart'; - // Set up initial configuration - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=android,ios', - // The below args aren't needed unless running from CI. We need for Github actions to run command. - '--platforms=android,ios,macos,web,windows', - '--web-app-id=$webAppId', - '--windows-app-id=$windowsAppId', - // Output to different file - '--out=lib/$configurationFileName', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - if (result.exitCode != 0) { - fail(result.stderr as String); - } - - final firebaseOptions = - p.join(projectPath!, 'lib', configurationFileName); - - // check "different_firebase_options.dart" file was recreated in lib directory - final firebaseOptionsContent = await File(firebaseOptions).readAsString(); - - final listOfStrings = firebaseOptionsContent.split('\n'); - expect( - listOfStrings, - containsAll([ - contains(appleAppId), - contains(appleBundleId), - contains(androidAppId), - contains(webAppId), - contains(windowsAppId), - contains('static const FirebaseOptions android = FirebaseOptions'), - contains('static const FirebaseOptions ios = FirebaseOptions'), - contains('static const FirebaseOptions web = FirebaseOptions'), - contains('static const FirebaseOptions windows = FirebaseOptions'), - ]), - ); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test('flutterfire configure: incorrect `--web-app-id` should throw exception', - () async { - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=web', - '--web-app-id=a-non-existent-web-app-id', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - final output = result.stderr as String; - final expectedOutput = [ - 'does not match the web app id of any existing Firebase app', - 'Exception', - ]; - - final expected = expectedOutput.every(output.contains); - expect(result.exitCode != 0, isTrue); - expect( - expected, - isTrue, - ); - }); - - test( - 'flutterfire configure: incorrect `--windows-app-id` should throw exception', - () async { - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=windows', - // Trigger the exception - '--windows-app-id=a-non-existent-windows-app-id', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - final output = result.stderr as String; - final expectedOutput = [ - 'does not match the web app id of any existing Firebase app', - 'Exception', - ]; - - final expected = expectedOutput.every(output.contains); - - expect(result.exitCode != 0, isTrue); - expect( - expected, - isTrue, - ); - }); - - test( - 'flutterfire configure: get correct Firebase App with manually created Firebase web app via `--web-app-id`', - () async { - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=web', - '--web-app-id=$secondWebAppId', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - expect(result.exitCode, 0); - // Console out put looks like this on success: "web 1:262904632156:web:cb3a00412ed430ca2f2799" - expect( - (result.stdout as String).contains( - 'web $secondWebAppId', + }, + skip: !Platform.isMacOS, + timeout: const Timeout( + Duration(minutes: 3), ), - isTrue, ); - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); - - await testFirebaseOptionsFileValues( - firebaseOptions, - selectedPlatform: kWeb, - ); - }); - - test( - 'flutterfire configure: get correct Firebase App with manually created Firebase web app via `--windows-app-id`', - () async { - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=windows', - '--windows-app-id=$secondWindowsAppId', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - expect(result.exitCode, 0); - - // check "firebase_options.dart" file is created in lib directory - final firebaseOptions = - p.join(projectPath!, 'lib', 'firebase_options.dart'); - - await testFirebaseOptionsFileValues( - firebaseOptions, - selectedPlatform: kWindows, - ); - }); - - test( - 'flutterfire configure: check Dart file configuration is updated correctly', - () async { - // Set up initial configuration - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - // Only configuring android initially - '--platforms=android', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - if (result.exitCode != 0) { - fail(result.stderr as String); - } - - final firebaseOptions = p.join( - projectPath!, - 'lib', - 'firebase_options.dart', - ); - - // check "firebase_options.dart" file was created and updates android only - final firebaseOptionsContent = await File(firebaseOptions).readAsString(); - - // only configure android initially - final listOfStrings = firebaseOptionsContent.split('\n'); - expect( - listOfStrings, - containsAll([ - contains(androidAppId), - contains('static const FirebaseOptions android = FirebaseOptions'), - ]), - ); - expect( - firebaseOptionsContent - .contains('static const FirebaseOptions web = FirebaseOptions'), - isFalse, - ); - expect( - firebaseOptionsContent - .contains('static const FirebaseOptions ios = FirebaseOptions'), - isFalse, - ); - - // Now reconfigure with ios, macos & web platforms and check Dart file is updated correctly - final result2 = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - // Configure the rest of the platforms - '--platforms=ios,macos,web', - '--web-app-id=$webAppId', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - if (result2.exitCode != 0) { - fail(result.stderr as String); - } - - final firebaseOptionsContent2 = - await File(firebaseOptions).readAsString(); - final listOfStrings2 = firebaseOptionsContent2.split('\n'); - - expect( - listOfStrings2, - containsAll([ - contains(androidAppId), - contains('static const FirebaseOptions web = FirebaseOptions'), - contains('static const FirebaseOptions android = FirebaseOptions'), - contains('static const FirebaseOptions ios = FirebaseOptions'), - ]), - ); - - final startIndexWeb = listOfStrings2.indexWhere( - (line) => line.contains( - 'if (kIsWeb)', - ), - ); - - listOfStrings2[startIndexWeb + 1].contains( - 'return web;', - ); - - final startIndexAndroid = listOfStrings2.indexWhere( - (line) => line.contains( - 'case TargetPlatform.android:', - ), - ); - - listOfStrings2[startIndexAndroid + 1].contains( - 'return android;', - ); - - final startIndexIos = listOfStrings2.indexWhere( - (line) => line.contains( - 'case TargetPlatform.iOS:', - ), - ); - - listOfStrings2[startIndexIos + 1].contains( - 'return ios;', - ); - - final startIndexMacos = listOfStrings2.indexWhere( - (line) => line.contains( - 'case TargetPlatform.macOS:', - ), - ); - - listOfStrings2[startIndexMacos + 1].contains( - 'return macos;', - ); - - // Now reconfigure with different apps across platforms and check Dart file is updated correctly - final result3 = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=ios,macos,web,android,windows', - '--ios-bundle-id=$secondAppleBundleId', - '--android-package-name=$secondAndroidApplicationId', - '--macos-bundle-id=$secondAppleBundleId', - '--web-app-id=$secondWebAppId', - '--windows-app-id=$secondWindowsAppId', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - if (result3.exitCode != 0) { - fail(result.stderr as String); - } - - final firebaseOptionsContent3 = - await File(firebaseOptions).readAsString(); - final listOfStrings3 = firebaseOptionsContent3.split('\n'); - - expect( - listOfStrings3, - containsAll([ - contains(secondAndroidAppId), - contains(secondAppleAppId), - contains(secondWebAppId), - contains(secondWindowsAppId), - ]), - ); - }, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test( - 'flutterfire configure: ensure android build.gradle files are only updated once', + test( + 'Validate `flutterfire upload-crashlytics-symbols` script is ran when building app', () async { - // Add crashlytics and performance to check they are only created once - final result = Process.runSync( - 'flutter', - ['pub', 'add', 'firebase_crashlytics', 'firebase_performance'], - workingDirectory: projectPath, - ); - - if (result.exitCode != 0) { - fail(result.stderr as String); - } - // Run first time to update - Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - // Only android - '--platforms=android', - ], - workingDirectory: projectPath, - runInShell: true, - ); - // Run second time and check it was only updated once - final result2 = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - // Only android - '--platforms=android', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - if (result2.exitCode != 0) { - fail(result2.stderr as String); - } - - await checkBuildGradleFileUpdated( - projectPath!, - checkCrashlytics: true, - checkPerf: true, - ); - }); - - test( - 'flutterfire configure: path with spaces should not break the configuration for iOS/macOS', - () async { - // Regression test for https://github.com/invertase/flutterfire_cli/pull/228 - const targetType = 'Runner'; - const iOSPath = 'ios path with spaces/target'; - const macOSPath = 'macos path with spaces/target'; - final result = Process.runSync( - 'flutterfire', - [ - 'configure', - '--yes', - '--project=$firebaseProjectId', - // The below args aren't needed unless running from CI. We need for Github actions to run command. - '--platforms=ios,macos', - // Apple required the `--ios-out` and `--macos-out` flags to be set & the build type, - // We're using `Runner` target for both which is the standard target for an apple Flutter app - '--ios-out=ios/$iOSPath', - '--ios-target=$targetType', - '--macos-out=macos/$macOSPath', - '--macos-target=$targetType', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - if (result.exitCode != 0) { - fail(result.stderr as String); - } - - if (Platform.isMacOS) { - // check Apple service files were created and have correct content - final iosPath = - p.join(projectPath!, kIos, iOSPath, appleServiceFileName); - final macosPath = p.join(projectPath!, kMacos, macOSPath); - - await testAppleServiceFileValues(iosPath); - await testAppleServiceFileValues( - macosPath, - platform: kMacos, + final result = Process.runSync( + 'flutter', + ['pub', 'add', 'firebase_crashlytics'], + workingDirectory: projectPath, ); - } - }, - skip: !Platform.isMacOS, - timeout: const Timeout( - Duration(minutes: 2), - ), - ); - - test( - 'Validate dev dependency works as normal & `flutterfire upload-crashlytics-symbols` works via dev dependency', - () async { - final result = Process.runSync( - 'flutter', - ['pub', 'add', 'firebase_crashlytics'], - workingDirectory: projectPath, - ); - - if (result.exitCode != 0) { - fail(result.stderr as String); - } - final installDevDependency = Process.runSync( - 'flutter', - [ - 'pub', - 'add', - '--dev', - 'flutterfire_cli', - '--path=${Directory.current.path}', - ], - workingDirectory: projectPath, - ); - - if (installDevDependency.exitCode != 0) { - fail(installDevDependency.stderr as String); - } - - final result2 = Process.runSync( - 'dart', - [ - 'run', - 'flutterfire_cli:flutterfire', - 'configure', - '--yes', - '--project=$firebaseProjectId', - '--platforms=ios,macos', - // Need to test bundle script is written correctly for both build configurations - '--ios-build-config=$appleBuildConfiguration', - '--ios-out=ios/$buildType', - '--macos-build-config=$appleBuildConfiguration', - '--macos-out=macos/$buildType', - ], - workingDirectory: projectPath, - runInShell: true, - ); - - if (result2.exitCode != 0) { - fail(result2.stderr as String); - } - - // Run grep to check for both strings - final grepDevDependencyScriptsAdded = Process.runSync( - 'grep', - [ - '-q', - '-E', - 'dart run flutterfire_cli:flutterfire (upload-crashlytics-symbols|bundle-service-file)', - 'ios/Runner.xcodeproj/project.pbxproj', - ], - workingDirectory: projectPath, - ); - - // Exit code 0 means both strings were found - expect( - grepDevDependencyScriptsAdded.exitCode, - 0, - reason: 'Required FlutterFire scripts not found in project.pbxproj', - ); + if (result.exitCode != 0) { + fail(result.stderr as String); + } - const iosVersion = '13.0'; - // Update project.pbxproj - final pbxprojResult = Process.runSync( - 'sed', - [ - '-i', - '', - 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = $iosVersion;/', - 'ios/Runner.xcodeproj/project.pbxproj', - ], - workingDirectory: projectPath, - ); + final result2 = Process.runSync( + 'flutterfire', + [ + 'configure', + '--yes', + '--project=$firebaseProjectId', + '--platforms=android,ios,macos,web,windows', + '--web-app-id=$webAppId', + '--windows-app-id=$windowsAppId', + ], + workingDirectory: projectPath, + runInShell: true, + ); - if (pbxprojResult.exitCode != 0) { - fail(pbxprojResult.stderr as String); - } + if (result2.exitCode != 0) { + fail(result2.stderr as String); + } - final buildApp = Process.runSync( - 'flutter', - [ + // Project is created with iOS 15.0 deployment target, no manual update needed + // Find iOS 26.0 simulator and use it + final buildArgs = [ 'build', 'ios', '--no-codesign', '--simulator', '--debug', '--verbose', - ], - workingDirectory: projectPath, - runInShell: true, - ); + ]; + buildArgs.add('--device-id=98641507-FA54-4F7C-BBE9-4970C7C0EB55'); - if (buildApp.exitCode != 0) { - fail(buildApp.stderr as String); - } + final buildApp = Process.runSync( + 'flutter', + buildArgs, + workingDirectory: projectPath, + runInShell: true, + ); - expect( - buildApp.stdout, - // Check symbols are uploaded in the background - contains('Symbol uploading will proceed in the background'), - ); + if (buildApp.exitCode != 0) { + fail(buildApp.stderr as String); + } - Process.runSync( - 'flutter', - ['config', '--enable-swift-package-manager'], - workingDirectory: projectPath, - ); + expect( + buildApp.stdout, + // Check symbols are uploaded in the background + contains('Symbol uploading will proceed in the background'), + ); - final iosDirectory = p.join(projectPath!, 'ios'); + Process.runSync( + 'flutter', + ['config', '--enable-swift-package-manager'], + workingDirectory: projectPath, + ); - Process.runSync( - 'bash', - [ - '-c', - '[ -f Podfile ] && rm Podfile && pod deintegrate && rm -rf Pods/', - ], - workingDirectory: iosDirectory, - ); + final iosDirectory = p.join(projectPath!, 'ios'); - final buildAppSPM = Process.runSync( - 'flutter', - [ - 'build', - 'ios', - '--no-codesign', - '--simulator', - '--debug', - '--verbose', - ], - workingDirectory: projectPath, - runInShell: true, - ); + Process.runSync( + 'bash', + [ + '-c', + '[ -f Podfile ] && rm Podfile && pod deintegrate && rm -rf Pods/', + ], + workingDirectory: iosDirectory, + ); - if (buildAppSPM.exitCode != 0) { - fail(buildAppSPM.stderr as String); - } + final buildAppSPM = Process.runSync( + 'flutter', + [ + 'build', + 'ios', + '--no-codesign', + '--simulator', + '--debug', + '--verbose', + ], + workingDirectory: projectPath, + runInShell: true, + ); - expect( - buildAppSPM.stdout, - // Check symbols are uploaded in the background - contains('Symbol uploading will proceed in the background'), - ); - }, - skip: !Platform.isMacOS, - timeout: const Timeout( - Duration(minutes: 3), - ), - ); + if (buildAppSPM.exitCode != 0) { + fail(buildAppSPM.stderr as String); + } + + expect( + buildAppSPM.stdout, + // Check symbols are uploaded in the background + contains('Symbol uploading will proceed in the background'), + ); + }, + skip: !Platform.isMacOS, + timeout: const Timeout( + Duration(minutes: 3), + ), + ); + }); } diff --git a/packages/flutterfire_cli/test/test_utils.dart b/packages/flutterfire_cli/test/test_utils.dart index 606b7891..9ebab701 100644 --- a/packages/flutterfire_cli/test/test_utils.dart +++ b/packages/flutterfire_cli/test/test_utils.dart @@ -43,7 +43,7 @@ const androidAppGradleUpdate = ''' // END: FlutterFire Configuration '''; -Future createFlutterProject() async { +Future createFlutterProject({bool updateiOSBuildTarget = false}) async { final tempDir = Directory.systemTemp.createTempSync(); const flutterProject = 'flutter_test_cli'; await Process.run( @@ -55,6 +55,35 @@ Future createFlutterProject() async { final flutterProjectPath = p.join(tempDir.path, flutterProject); + if (updateiOSBuildTarget && Platform.isMacOS) { + // Set iOS minimum deployment target to 15.0 + const iosVersion = '15.0'; + final pbxprojPath = p.join( + flutterProjectPath, + 'ios', + 'Runner.xcodeproj', + 'project.pbxproj', + ); + + // Update project.pbxproj - use 'g' flag to replace ALL occurrences + final pbxprojResult = await Process.run( + 'sed', + [ + '-i', + '', + 's/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*;/IPHONEOS_DEPLOYMENT_TARGET = $iosVersion;/g', + pbxprojPath, + ], + workingDirectory: flutterProjectPath, + ); + + if (pbxprojResult.exitCode != 0) { + throw Exception( + 'Failed to set iOS deployment target: ${pbxprojResult.stderr}', + ); + } + } + return flutterProjectPath; } diff --git a/packages/flutterfire_cli/test/unit_test.dart b/packages/flutterfire_cli/test/unit_test.dart index 292473d8..a804d6d2 100644 --- a/packages/flutterfire_cli/test/unit_test.dart +++ b/packages/flutterfire_cli/test/unit_test.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutterfire_cli/src/common/strings.dart'; import 'package:flutterfire_cli/src/common/utils.dart'; +import 'package:flutterfire_cli/src/firebase/firebase_app.dart'; import 'package:test/test.dart'; void main() { @@ -142,6 +143,49 @@ void main() { }); }); + group('FirebaseApp displayName handling', () { + test('FirebaseApp.fromJson correctly parses displayName from JSON', () { + final json = { + 'platform': 'ANDROID', + 'appId': '1:123456:android:abc', + 'displayName': 'My Cool App', + 'name': 'projects/test/apps/abc', + 'packageName': 'com.example.app', + }; + + final app = FirebaseApp.fromJson(json); + + expect(app.displayName, 'My Cool App'); + expect(app.name, 'projects/test/apps/abc'); + expect(app.platform, 'android'); + expect(app.packageNameOrBundleIdentifier, 'com.example.app'); + }); + + test('FirebaseApp.fromJson handles null or missing displayName', () { + // Test with null displayName + final jsonWithNull = { + 'platform': 'IOS', + 'appId': '1:123456:ios:xyz', + 'displayName': null, + 'name': 'projects/test/apps/xyz', + 'bundleId': 'com.example.app', + }; + + final appWithNull = FirebaseApp.fromJson(jsonWithNull); + expect(appWithNull.displayName, null); + + // Test with missing displayName field + final jsonWithoutField = { + 'platform': 'WEB', + 'appId': '1:123456:web:def', + 'name': 'projects/test/apps/def', + }; + + final appWithoutField = FirebaseApp.fromJson(jsonWithoutField); + expect(appWithoutField.displayName, null); + }); + }); + group( 'Firebase CLI JSON response parser function `firebaseCLIJsonParse()`', () {