aboutsummaryrefslogtreecommitdiffhomepage
path: root/Example/Core
diff options
context:
space:
mode:
authorGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
committerGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
commit98ba64449a632518bd2b86fe8d927f4a960d3ddc (patch)
tree131d9c4272fa6179fcda6c5a33fcb3b1bd57ad2e /Example/Core
parent32461366c9e204a527ca05e6e9b9404a2454ac51 (diff)
Initial
Diffstat (limited to 'Example/Core')
-rw-r--r--Example/Core/App/Base.lproj/LaunchScreen.storyboard27
-rw-r--r--Example/Core/App/Base.lproj/Main.storyboard27
-rw-r--r--Example/Core/App/Core-Info.plist49
-rw-r--r--Example/Core/App/FIRAppDelegate.h23
-rw-r--r--Example/Core/App/FIRAppDelegate.m52
-rw-r--r--Example/Core/App/FIRViewController.h21
-rw-r--r--Example/Core/App/FIRViewController.m35
-rw-r--r--Example/Core/App/GoogleService-Info.plist30
-rw-r--r--Example/Core/App/main.m23
-rw-r--r--Example/Core/Tests/FIRAppAssociationRegistrationUnitTests.m193
-rw-r--r--Example/Core/Tests/FIRAppTest.m582
-rw-r--r--Example/Core/Tests/FIRBundleUtilTest.m86
-rw-r--r--Example/Core/Tests/FIRConfigurationTest.m31
-rw-r--r--Example/Core/Tests/FIRLoggerTest.m265
-rw-r--r--Example/Core/Tests/FIROptionsTest.m468
-rw-r--r--Example/Core/Tests/FIRTestCase.h45
-rw-r--r--Example/Core/Tests/FIRTestCase.m47
-rw-r--r--Example/Core/Tests/Tests-Info.plist22
18 files changed, 2026 insertions, 0 deletions
diff --git a/Example/Core/App/Base.lproj/LaunchScreen.storyboard b/Example/Core/App/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..66a7681
--- /dev/null
+++ b/Example/Core/App/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Core/App/Base.lproj/Main.storyboard b/Example/Core/App/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..d164a23
--- /dev/null
+++ b/Example/Core/App/Base.lproj/Main.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="7706" systemVersion="14D136" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="whP-gf-Uak">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="wQg-tq-qST">
+ <objects>
+ <viewController id="whP-gf-Uak" customClass="FIRViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="uEw-UM-LJ8"/>
+ <viewControllerLayoutGuide type="bottom" id="Mvr-aV-6Um"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="TpU-gO-2f1">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="tc2-Qw-aMS" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="305" y="433"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Core/App/Core-Info.plist b/Example/Core/App/Core-Info.plist
new file mode 100644
index 0000000..7576a0d
--- /dev/null
+++ b/Example/Core/App/Core-Info.plist
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/Example/Core/App/FIRAppDelegate.h b/Example/Core/App/FIRAppDelegate.h
new file mode 100644
index 0000000..e3fba8f
--- /dev/null
+++ b/Example/Core/App/FIRAppDelegate.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import UIKit;
+
+@interface FIRAppDelegate : UIResponder <UIApplicationDelegate>
+
+@property (strong, nonatomic) UIWindow *window;
+
+@end
diff --git a/Example/Core/App/FIRAppDelegate.m b/Example/Core/App/FIRAppDelegate.m
new file mode 100644
index 0000000..0ecfdea
--- /dev/null
+++ b/Example/Core/App/FIRAppDelegate.m
@@ -0,0 +1,52 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRAppDelegate.h"
+
+@implementation FIRAppDelegate
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
+{
+ // Override point for customization after application launch.
+ return YES;
+}
+
+- (void)applicationWillResignActive:(UIApplication *)application
+{
+ // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
+ // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
+}
+
+- (void)applicationDidEnterBackground:(UIApplication *)application
+{
+ // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
+ // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
+}
+
+- (void)applicationWillEnterForeground:(UIApplication *)application
+{
+ // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
+}
+
+- (void)applicationDidBecomeActive:(UIApplication *)application
+{
+ // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+}
+
+- (void)applicationWillTerminate:(UIApplication *)application
+{
+ // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
+}
+
+@end
diff --git a/Example/Core/App/FIRViewController.h b/Example/Core/App/FIRViewController.h
new file mode 100644
index 0000000..64b4b74
--- /dev/null
+++ b/Example/Core/App/FIRViewController.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import UIKit;
+
+@interface FIRViewController : UIViewController
+
+@end
diff --git a/Example/Core/App/FIRViewController.m b/Example/Core/App/FIRViewController.m
new file mode 100644
index 0000000..901accf
--- /dev/null
+++ b/Example/Core/App/FIRViewController.m
@@ -0,0 +1,35 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRViewController.h"
+
+@interface FIRViewController ()
+
+@end
+
+@implementation FIRViewController
+
+- (void)viewDidLoad
+{
+ [super viewDidLoad];
+ // Do any additional setup after loading the view, typically from a nib.
+}
+
+- (void)didReceiveMemoryWarning
+{
+ [super didReceiveMemoryWarning];
+ // Dispose of any resources that can be recreated.
+}
+
+@end
diff --git a/Example/Core/App/GoogleService-Info.plist b/Example/Core/App/GoogleService-Info.plist
new file mode 100644
index 0000000..89afffe
--- /dev/null
+++ b/Example/Core/App/GoogleService-Info.plist
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>API_KEY</key>
+ <string>correct_api_key</string>
+ <key>TRACKING_ID</key>
+ <string>correct_tracking_id</string>
+ <key>CLIENT_ID</key>
+ <string>correct_client_id</string>
+ <key>REVERSED_CLIENT_ID</key>
+ <string>correct_reversed_client_id</string>
+ <key>ANDROID_CLIENT_ID</key>
+ <string>correct_android_client_id</string>
+ <key>GOOGLE_APP_ID</key>
+ <string>1:123:ios:123abc</string>
+ <key>GCM_SENDER_ID</key>
+ <string>correct_gcm_sender_id</string>
+ <key>PLIST_VERSION</key>
+ <string>1</string>
+ <key>BUNDLE_ID</key>
+ <string>com.google.FirebaseSDKTests</string>
+ <key>PROJECT_ID</key>
+ <string>abc-xyz-123</string>
+ <key>DATABASE_URL</key>
+ <string>https://abc-xyz-123.firebaseio.com</string>
+ <key>STORAGE_BUCKET</key>
+ <string>project-id-123.storage.firebase.com</string>
+</dict>
+</plist>
diff --git a/Example/Core/App/main.m b/Example/Core/App/main.m
new file mode 100644
index 0000000..03b5c12
--- /dev/null
+++ b/Example/Core/App/main.m
@@ -0,0 +1,23 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@import UIKit;
+#import "FIRAppDelegate.h"
+
+int main(int argc, char * argv[])
+{
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([FIRAppDelegate class]));
+ }
+}
diff --git a/Example/Core/Tests/FIRAppAssociationRegistrationUnitTests.m b/Example/Core/Tests/FIRAppAssociationRegistrationUnitTests.m
new file mode 100644
index 0000000..9649c99
--- /dev/null
+++ b/Example/Core/Tests/FIRAppAssociationRegistrationUnitTests.m
@@ -0,0 +1,193 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <XCTest/XCTest.h>
+
+#import "FIRAppAssociationRegistration.h"
+
+/** @var kKey
+ @brief A unique string key.
+ */
+static NSString *kKey = @"key";
+
+/** @var kKey1
+ @brief A unique string key.
+ */
+static NSString *kKey1 = @"key1";
+
+/** @var kKey2
+ @brief A unique string key.
+ */
+static NSString *kKey2 = @"key2";
+
+/** @var gCreateNewObject
+ @brief A block that returns a new object everytime it is called.
+ */
+static id _Nullable (^gCreateNewObject)() = ^id _Nullable() {
+ return [[NSObject alloc] init];
+};
+
+/** @class FIRAppAssociationRegistrationTests
+ @brief Tests for @c FIRAppAssociationRegistration
+ */
+@interface FIRAppAssociationRegistrationTests : XCTestCase
+@end
+
+@implementation FIRAppAssociationRegistrationTests
+
+- (void)testPassObject {
+ id host = gCreateNewObject();
+ id obj = gCreateNewObject();
+ id result = [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:^id _Nullable() {
+ return obj;
+ }];
+ XCTAssertEqual(obj, result);
+}
+
+- (void)testPassNil {
+ id host = gCreateNewObject();
+ id obj = [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:^id _Nullable() {
+ return nil;
+ }];
+ XCTAssertNil(obj);
+}
+
+- (void)testObjectOwnership {
+ __weak id weakHost;
+ __block __weak id weakObj;
+ @autoreleasepool {
+ id host = gCreateNewObject();
+ weakHost = host;
+ [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:^id _Nullable() {
+ id obj = gCreateNewObject();
+ weakObj = obj;
+ return obj;
+ }];
+ // Verify that neither the host nor the object is released yet, i.e., the host owns the object
+ // because nothing else retains the object.
+ XCTAssertNotNil(weakHost);
+ XCTAssertNotNil(weakObj);
+ }
+ // Verify that both the host and the object are released upon exit of the autorelease pool,
+ // i.e., the host is the sole owner of the object.
+ XCTAssertNil(weakHost);
+ XCTAssertNil(weakObj);
+}
+
+- (void)testSameHostSameKey {
+ id host = gCreateNewObject();
+ id obj1 = [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:gCreateNewObject];
+ id obj2 = [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:gCreateNewObject];
+ XCTAssertEqual(obj1, obj2);
+}
+
+- (void)testSameHostDifferentKey {
+ id host = gCreateNewObject();
+ id obj1 = [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey1
+ creationBlock:gCreateNewObject];
+ id obj2 = [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey2
+ creationBlock:gCreateNewObject];
+ XCTAssertNotEqual(obj1, obj2);
+}
+
+- (void)testDifferentHostSameKey {
+ id host1 = gCreateNewObject();
+ id obj1 = [FIRAppAssociationRegistration registeredObjectWithHost:host1
+ key:kKey
+ creationBlock:gCreateNewObject];
+ id host2 = gCreateNewObject();
+ id obj2 = [FIRAppAssociationRegistration registeredObjectWithHost:host2
+ key:kKey
+ creationBlock:gCreateNewObject];
+ XCTAssertNotEqual(obj1, obj2);
+}
+
+- (void)testDifferentHostDifferentKey {
+ id host1 = gCreateNewObject();
+ id obj1 = [FIRAppAssociationRegistration registeredObjectWithHost:host1
+ key:kKey1
+ creationBlock:gCreateNewObject];
+ id host2 = gCreateNewObject();
+ id obj2 = [FIRAppAssociationRegistration registeredObjectWithHost:host2
+ key:kKey2
+ creationBlock:gCreateNewObject];
+ XCTAssertNotEqual(obj1, obj2);
+}
+
+- (void)testReentrySameHostSameKey {
+ id host = gCreateNewObject();
+ XCTAssertThrows([FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:^id _Nullable() {
+ [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey
+ creationBlock:gCreateNewObject];
+ return gCreateNewObject();
+ }]);
+}
+
+- (void)testReentrySameHostDifferentKey {
+ id host = gCreateNewObject();
+ [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey1
+ creationBlock:^id _Nullable() {
+ [FIRAppAssociationRegistration registeredObjectWithHost:host
+ key:kKey2
+ creationBlock:gCreateNewObject];
+ return gCreateNewObject();
+ }];
+ // Expect no exception raised.
+}
+
+- (void)testReentryDifferentHostSameKey {
+ id host1 = gCreateNewObject();
+ id host2 = gCreateNewObject();
+ [FIRAppAssociationRegistration registeredObjectWithHost:host1
+ key:kKey
+ creationBlock:^id _Nullable() {
+ [FIRAppAssociationRegistration registeredObjectWithHost:host2
+ key:kKey
+ creationBlock:gCreateNewObject];
+ return gCreateNewObject();
+ }];
+ // Expect no exception raised.
+}
+
+- (void)testReentryDifferentHostDifferentKey {
+ id host1 = gCreateNewObject();
+ id host2 = gCreateNewObject();
+ [FIRAppAssociationRegistration registeredObjectWithHost:host1
+ key:kKey1
+ creationBlock:^id _Nullable() {
+ [FIRAppAssociationRegistration registeredObjectWithHost:host2
+ key:kKey2
+ creationBlock:gCreateNewObject];
+ return gCreateNewObject();
+ }];
+ // Expect no exception raised.
+}
+
+@end
diff --git a/Example/Core/Tests/FIRAppTest.m b/Example/Core/Tests/FIRAppTest.m
new file mode 100644
index 0000000..da97c6c
--- /dev/null
+++ b/Example/Core/Tests/FIRAppTest.m
@@ -0,0 +1,582 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRAppInternal.h"
+#import "FIROptionsInternal.h"
+#import "FIRTestCase.h"
+
+NSString *const kFIRTestAppName1 = @"test_app_name_1";
+NSString *const kFIRTestAppName2 = @"test-app-name-2";
+
+@interface FIRApp (TestInternal)
+
+@property(nonatomic) BOOL alreadySentConfigureNotification;
+@property(nonatomic) BOOL alreadySentDeleteNotification;
+
++ (void)resetApps;
+- (instancetype)initInstanceWithName:(NSString *)name options:(FIROptions *)options;
+- (BOOL)configureCore;
++ (NSError *)errorForInvalidAppID;
+- (BOOL)isAppIDValid;
++ (NSString *)actualBundleID;
++ (NSNumber *)mapFromServiceStringToTypeEnum:(NSString *)serviceString;
++ (NSString *)deviceModel;
++ (NSString *)installString;
++ (NSURL *)filePathURLWithName:(NSString *)fileName;
++ (NSString *)stringAtURL:(NSURL *)filePathURL;
++ (BOOL)writeString:(NSString *)string toURL:(NSURL *)filePathURL;
++ (void)logAppInfo:(NSNotification *)notification;
++ (BOOL)validateAppID:(NSString *)appID;
++ (BOOL)validateAppIDFormat:(NSString *)appID withVersion:(NSString *)version;
++ (BOOL)validateAppIDFingerprint:(NSString *)appID withVersion:(NSString *)version;
+
+@end
+
+
+@interface FIRAppTest : FIRTestCase
+
+@property(nonatomic) id appClassMock;
+@property(nonatomic) id optionsInstanceMock;
+@property(nonatomic) id notificationCenterMock;
+@property(nonatomic) FIRApp *app;
+
+@end
+
+@implementation FIRAppTest
+
+- (void)setUp {
+ [super setUp];
+ [FIROptions resetDefaultOptions];
+ [FIRApp resetApps];
+ _appClassMock = OCMClassMock([FIRApp class]);
+ _optionsInstanceMock = OCMPartialMock([FIROptions defaultOptions]);
+ _notificationCenterMock = OCMPartialMock([NSNotificationCenter defaultCenter]);
+}
+
+- (void)tearDown {
+ [_appClassMock stopMocking];
+ [_optionsInstanceMock stopMocking];
+ [_notificationCenterMock stopMocking];
+
+ [super tearDown];
+}
+
+- (void)testConfigure {
+ NSDictionary *expectedUserInfo = [self expectedUserInfoWithAppName:kFIRDefaultAppName
+ isDefaultApp:YES];
+ OCMExpect([self.notificationCenterMock postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ userInfo:expectedUserInfo]);
+ XCTAssertNoThrow([FIRApp configure]);
+ OCMVerifyAll(self.notificationCenterMock);
+
+ self.app = [FIRApp defaultApp];
+ XCTAssertNotNil(self.app);
+ XCTAssertEqualObjects(self.app.name, kFIRDefaultAppName);
+ XCTAssertEqualObjects(self.app.options.clientID, kClientID);
+ XCTAssertTrue([FIRApp allApps].count == 1);
+ XCTAssertTrue(self.app.alreadySentConfigureNotification);
+
+ // Test if options is nil
+ id optionsClassMock = OCMClassMock([FIROptions class]);
+ OCMStub([optionsClassMock defaultOptions]).andReturn(nil);
+ XCTAssertThrows([FIRApp configure]);
+}
+
+- (void)testConfigureWithOptions {
+ // nil options
+ XCTAssertThrows([FIRApp configureWithOptions:nil]);
+ XCTAssertTrue([FIRApp allApps].count == 0);
+
+ NSDictionary *expectedUserInfo = [self expectedUserInfoWithAppName:kFIRDefaultAppName
+ isDefaultApp:YES];
+ OCMExpect([self.notificationCenterMock postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ userInfo:expectedUserInfo]);
+ // default options
+ XCTAssertNoThrow([FIRApp configureWithOptions:[FIROptions defaultOptions]]);
+ OCMVerifyAll(self.notificationCenterMock);
+
+ self.app = [FIRApp defaultApp];
+ XCTAssertNotNil(self.app);
+ XCTAssertEqualObjects(self.app.name, kFIRDefaultAppName);
+ XCTAssertEqualObjects(self.app.options.clientID, kClientID);
+ XCTAssertTrue([FIRApp allApps].count == 1);
+}
+
+- (void)testConfigureWithCustomizedOptions {
+ // valid customized options
+ FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ bundleID:kBundleID
+ GCMSenderID:kGCMSenderID
+ APIKey:kCustomizedAPIKey
+ clientID:nil
+ trackingID:nil
+ androidClientID:nil
+ databaseURL:nil
+ storageBucket:nil
+ deepLinkURLScheme:nil];
+
+ NSDictionary *expectedUserInfo = [self expectedUserInfoWithAppName:kFIRDefaultAppName
+ isDefaultApp:YES];
+ OCMExpect([self.notificationCenterMock postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ userInfo:expectedUserInfo]);
+
+ XCTAssertNoThrow([FIRApp configureWithOptions:options]);
+ OCMVerifyAll(self.notificationCenterMock);
+
+ self.app = [FIRApp defaultApp];
+ XCTAssertNotNil(self.app);
+ XCTAssertEqualObjects(self.app.name, kFIRDefaultAppName);
+ XCTAssertEqualObjects(self.app.options.googleAppID, kGoogleAppID);
+ XCTAssertEqualObjects(self.app.options.APIKey, kCustomizedAPIKey);
+ XCTAssertTrue([FIRApp allApps].count == 1);
+}
+
+- (void)testConfigureWithNameAndOptions {
+ XCTAssertThrows([FIRApp configureWithName:nil options:[FIROptions defaultOptions]]);
+ XCTAssertThrows([FIRApp configureWithName:kFIRTestAppName1 options:nil]);
+ XCTAssertThrows([FIRApp configureWithName:@"" options:[FIROptions defaultOptions]]);
+ XCTAssertThrows([FIRApp configureWithName:kFIRDefaultAppName
+ options:[FIROptions defaultOptions]]);
+ XCTAssertTrue([FIRApp allApps].count == 0);
+
+ NSDictionary *expectedUserInfo = [self expectedUserInfoWithAppName:kFIRTestAppName1
+ isDefaultApp:NO];
+ OCMExpect([self.notificationCenterMock postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ userInfo:expectedUserInfo]);
+ XCTAssertNoThrow([FIRApp configureWithName:kFIRTestAppName1 options:[FIROptions defaultOptions]]);
+ OCMVerifyAll(self.notificationCenterMock);
+
+ XCTAssertTrue([FIRApp allApps].count == 1);
+ self.app = [FIRApp appNamed:kFIRTestAppName1];
+ XCTAssertNotNil(self.app);
+ XCTAssertEqualObjects(self.app.name, kFIRTestAppName1);
+ XCTAssertEqualObjects(self.app.options.clientID, kClientID);
+
+ // Configure the same app again should throw an exception.
+ XCTAssertThrows([FIRApp configureWithName:kFIRTestAppName1 options:[FIROptions defaultOptions]]);
+}
+
+- (void)testConfigureWithNameAndCustomizedOptions {
+ FIROptions *options = [FIROptions defaultOptions];
+ FIROptions *newOptions = [options copy];
+ newOptions.deepLinkURLScheme = kDeepLinkURLScheme;
+
+ NSDictionary *expectedUserInfo1 = [self expectedUserInfoWithAppName:kFIRTestAppName1
+ isDefaultApp:NO];
+ OCMExpect([self.notificationCenterMock postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ userInfo:expectedUserInfo1]);
+ XCTAssertNoThrow([FIRApp configureWithName:kFIRTestAppName1 options:newOptions]);
+ XCTAssertTrue([FIRApp allApps].count == 1);
+ self.app = [FIRApp appNamed:kFIRTestAppName1];
+
+ // Configure a different app with valid customized options
+ FIROptions *customizedOptions = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ bundleID:kBundleID
+ GCMSenderID:kGCMSenderID
+ APIKey:kCustomizedAPIKey
+ clientID:nil
+ trackingID:nil
+ androidClientID:nil
+ databaseURL:nil
+ storageBucket:nil
+ deepLinkURLScheme:nil];
+
+ NSDictionary *expectedUserInfo2 = [self expectedUserInfoWithAppName:kFIRTestAppName2
+ isDefaultApp:NO];
+ OCMExpect([self.notificationCenterMock postNotificationName:kFIRAppReadyToConfigureSDKNotification
+ object:[FIRApp class]
+ userInfo:expectedUserInfo2]);
+ XCTAssertNoThrow([FIRApp configureWithName:kFIRTestAppName2 options:customizedOptions]);
+ OCMVerifyAll(self.notificationCenterMock);
+
+ XCTAssertTrue([FIRApp allApps].count == 2);
+ self.app = [FIRApp appNamed:kFIRTestAppName2];
+ XCTAssertNotNil(self.app);
+ XCTAssertEqualObjects(self.app.name, kFIRTestAppName2);
+ XCTAssertEqualObjects(self.app.options.googleAppID, kGoogleAppID);
+ XCTAssertEqualObjects(self.app.options.APIKey, kCustomizedAPIKey);
+}
+
+- (void)testValidName {
+ XCTAssertNoThrow([FIRApp configureWithName:@"aA1_" options:[FIROptions defaultOptions]]);
+ XCTAssertThrows([FIRApp configureWithName:@"aA1%" options:[FIROptions defaultOptions]]);
+ XCTAssertThrows([FIRApp configureWithName:@"aA1?" options:[FIROptions defaultOptions]]);
+ XCTAssertThrows([FIRApp configureWithName:@"aA1!" options:[FIROptions defaultOptions]]);
+}
+
+- (void)testDefaultApp {
+ self.app = [FIRApp defaultApp];
+ XCTAssertNil(self.app);
+
+ [FIRApp configure];
+ self.app = [FIRApp defaultApp];
+ XCTAssertEqualObjects(self.app.name, kFIRDefaultAppName);
+ XCTAssertEqualObjects(self.app.options.clientID, kClientID);
+}
+
+- (void)testAppNamed {
+ self.app = [FIRApp appNamed:kFIRTestAppName1];
+ XCTAssertNil(self.app);
+
+ [FIRApp configureWithName:kFIRTestAppName1 options:[FIROptions defaultOptions]];
+ self.app = [FIRApp appNamed:kFIRTestAppName1];
+ XCTAssertEqualObjects(self.app.name, kFIRTestAppName1);
+ XCTAssertEqualObjects(self.app.options.clientID, kClientID);
+}
+
+- (void)testDeleteApp {
+ [FIRApp configure];
+ self.app = [FIRApp defaultApp];
+ XCTAssertTrue([FIRApp allApps].count == 1);
+ [self.app deleteApp:^(BOOL success) {
+ XCTAssertTrue(success);
+ }];
+ OCMVerify([self.notificationCenterMock postNotificationName:kFIRAppDeleteNotification
+ object:[FIRApp class]
+ userInfo:[OCMArg any]]);
+ XCTAssertTrue(self.app.alreadySentDeleteNotification);
+ XCTAssertTrue([FIRApp allApps].count == 0);
+}
+
+- (void)testErrorForSubspecConfigurationFailure {
+ NSError *error = [FIRApp errorForSubspecConfigurationFailureWithDomain:kFirebaseAdMobErrorDomain
+ errorCode:FIRErrorCodeAdMobFailed
+ service:kFIRServiceAdMob
+ reason:@"some reason"];
+ XCTAssertNotNil(error);
+ XCTAssert([error.domain isEqualToString:kFirebaseAdMobErrorDomain]);
+ XCTAssert(error.code == FIRErrorCodeAdMobFailed);
+ XCTAssert([error.description containsString:@"Configuration failed for"]);
+}
+
+- (void)testGetTokenWithCallback {
+ [FIRApp configure];
+ FIRApp *app = [FIRApp defaultApp];
+
+ __block BOOL getTokenImplementationWasCalled = NO;
+ __block BOOL getTokenCallbackWasCalled = NO;
+ __block BOOL passedRefreshValue = NO;
+
+ [app getTokenForcingRefresh:YES
+ withCallback:^(NSString *_Nullable token, NSError *_Nullable error) {
+ getTokenCallbackWasCalled = YES;
+ }];
+
+ XCTAssert(getTokenCallbackWasCalled,
+ @"The callback should be invoked by the base implementation when no block for "
+ "'getTokenImplementation' has been specified.");
+
+ getTokenCallbackWasCalled = NO;
+
+ app.getTokenImplementation = ^(BOOL refresh, FIRTokenCallback callback) {
+ getTokenImplementationWasCalled = YES;
+ passedRefreshValue = refresh;
+ callback(nil, nil);
+ };
+ [app getTokenForcingRefresh:YES
+ withCallback:^(NSString *_Nullable token, NSError *_Nullable error) {
+ getTokenCallbackWasCalled = YES;
+ }];
+
+ XCTAssert(getTokenImplementationWasCalled,
+ @"The 'getTokenImplementation' block was never called.");
+ XCTAssert(passedRefreshValue,
+ @"The value for the 'refresh' parameter wasn't passed to the 'getTokenImplementation' "
+ "block correctly.");
+ XCTAssert(getTokenCallbackWasCalled,
+ @"The 'getTokenImplementation' should have invoked the callback. This could be an "
+ "error in this test, or the callback parameter may not have been passed to the "
+ "implementation correctly.");
+
+ getTokenImplementationWasCalled = NO;
+ getTokenCallbackWasCalled = NO;
+ passedRefreshValue = NO;
+
+ [app getTokenForcingRefresh:NO
+ withCallback:^(NSString *_Nullable token, NSError *_Nullable error) {
+ getTokenCallbackWasCalled = YES;
+ }];
+
+ XCTAssertFalse(passedRefreshValue,
+ @"The value for the 'refresh' parameter wasn't passed to the "
+ "'getTokenImplementation' block correctly.");
+}
+
+- (void)testModifyingOptionsThrows {
+ [FIRApp configure];
+ FIROptions *options = [[FIRApp defaultApp] options];
+ XCTAssertTrue(options.isEditingLocked);
+
+ // Modification to every property should result in an exception.
+ XCTAssertThrows(options.androidClientID = @"should_throw");
+ XCTAssertThrows(options.APIKey = @"should_throw");
+ XCTAssertThrows(options.bundleID = @"should_throw");
+ XCTAssertThrows(options.clientID = @"should_throw");
+ XCTAssertThrows(options.databaseURL = @"should_throw");
+ XCTAssertThrows(options.deepLinkURLScheme = @"should_throw");
+ XCTAssertThrows(options.GCMSenderID = @"should_throw");
+ XCTAssertThrows(options.googleAppID = @"should_throw");
+ XCTAssertThrows(options.projectID = @"should_throw");
+ XCTAssertThrows(options.storageBucket = @"should_throw");
+ XCTAssertThrows(options.trackingID = @"should_throw");
+}
+
+- (void)testOptionsLocking {
+ FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ GCMSenderID:kGCMSenderID];
+ options.projectID = kProjectID;
+ options.databaseURL = kDatabaseURL;
+
+ // Options should not be locked before they are used to configure a `FIRApp`.
+ XCTAssertFalse(options.isEditingLocked);
+
+ // The options returned should be locked after configuring `FIRApp`.
+ [FIRApp configureWithOptions:options];
+ FIROptions *optionsCopy = [[FIRApp defaultApp] options];
+ XCTAssertTrue(optionsCopy.isEditingLocked);
+}
+
+#pragma mark - App ID v1
+
+- (void)testAppIDV1 {
+ // Missing separator between platform:fingerprint.
+ XCTAssertFalse([FIRApp validateAppID:@"1:1337:iosdeadbeef"]);
+
+ // Wrong platform "android".
+ XCTAssertFalse([FIRApp validateAppID:@"1:1337:android:deadbeef"]);
+
+ // The fingerprint, aka 4th field, should only contain hex characters.
+ XCTAssertFalse([FIRApp validateAppID:@"1:1337:ios:123abcxyz"]);
+
+ // The fingerprint, aka 4th field, is not tested in V1, so a bad value shouldn't cause a failure.
+ XCTAssertTrue([FIRApp validateAppID:@"1:1337:ios:deadbeef"]);
+}
+
+#pragma mark - App ID v2
+
+- (void)testAppIDV2 {
+ // Missing separator between platform:fingerprint.
+ XCTAssertTrue([FIRApp validateAppID:@"2:1337:ios5e18052ab54fbfec"]);
+
+ // Unknown versions may contain anything.
+ XCTAssertTrue([FIRApp validateAppID:@"2:1337:ios:123abcxyz"]);
+ XCTAssertTrue([FIRApp validateAppID:@"2:thisdoesn'teven_m:a:t:t:e:r_"]);
+
+ // Known good fingerprint.
+ XCTAssertTrue([FIRApp validateAppID:@"2:1337:ios:5e18052ab54fbfec"]);
+
+ // Unknown fingerprint, not tested so shouldn't cause a failure.
+ XCTAssertTrue([FIRApp validateAppID:@"2:1337:ios:deadbeef"]);
+}
+
+#pragma mark - App ID other
+
+- (void)testAppIDV3 {
+ // Currently there is no specification for v3, so we would not expect it to fail.
+ XCTAssertTrue([FIRApp validateAppID:@"3:1337:ios:deadbeef"]);
+}
+
+- (void)testAppIDEmpty {
+ XCTAssertFalse([FIRApp validateAppID:@""]);
+}
+
+- (void)testAppIDValidationTrue {
+ // Ensure that isAppIDValid matches validateAppID.
+ [FIRApp configure];
+ OCMStub([self.appClassMock validateAppID:[OCMArg any]]).andReturn(YES);
+ XCTAssertTrue([[FIRApp defaultApp] isAppIDValid]);
+}
+
+- (void)testAppIDValidationFalse {
+ // Ensure that isAppIDValid matches validateAppID.
+ [FIRApp configure];
+ OCMStub([self.appClassMock validateAppID:[OCMArg any]]).andReturn(NO);
+ XCTAssertFalse([[FIRApp defaultApp] isAppIDValid]);
+}
+
+- (void)testAppIDPrefix {
+ // Unknown numeric-character prefixes should pass.
+ XCTAssertTrue([FIRApp validateAppID:@"0:"]);
+ XCTAssertTrue([FIRApp validateAppID:@"01:"]);
+ XCTAssertTrue([FIRApp validateAppID:@"10:"]);
+ XCTAssertTrue([FIRApp validateAppID:@"010:"]);
+ XCTAssertTrue([FIRApp validateAppID:@"3:"]);
+ XCTAssertTrue([FIRApp validateAppID:@"123:"]);
+ XCTAssertTrue([FIRApp validateAppID:@"999999999:"]);
+
+ // Non-numeric prefixes should not pass.
+ XCTAssertFalse([FIRApp validateAppID:@"a:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"abcsdf0:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"0aaaa:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"0aaaa0450:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"-1:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"abcsdf:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"ABDCF:"]);
+ XCTAssertFalse([FIRApp validateAppID:@" :"]);
+ XCTAssertFalse([FIRApp validateAppID:@"1 :"]);
+ XCTAssertFalse([FIRApp validateAppID:@" 1:"]);
+ XCTAssertFalse([FIRApp validateAppID:@" 123 :"]);
+ XCTAssertFalse([FIRApp validateAppID:@"1 23:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"&($*&%(*$&:"]);
+ XCTAssertFalse([FIRApp validateAppID:@"abCDSF$%%df:"]);
+
+ // Known version prefixes should never pass without the rest of the app ID string present.
+ XCTAssertFalse([FIRApp validateAppID:@"1:"]);
+
+ // Version must include ":".
+ XCTAssertFalse([FIRApp validateAppID:@"0"]);
+ XCTAssertFalse([FIRApp validateAppID:@"01"]);
+ XCTAssertFalse([FIRApp validateAppID:@"10"]);
+ XCTAssertFalse([FIRApp validateAppID:@"010"]);
+ XCTAssertFalse([FIRApp validateAppID:@"3"]);
+ XCTAssertFalse([FIRApp validateAppID:@"123"]);
+ XCTAssertFalse([FIRApp validateAppID:@"999999999"]);
+ XCTAssertFalse([FIRApp validateAppID:@"com.google.bundleID"]);
+}
+
+- (void)testAppIDFormatInvalid {
+ OCMStub([self.appClassMock actualBundleID]).andReturn(@"com.google.bundleID");
+ // Some direct tests of the validateAppIDFormat:withVersion: method.
+ // Sanity checks first.
+ NSString *const kGoodAppIDV1 = @"1:1337:ios:deadbeef";
+ NSString *const kGoodVersionV1 = @"1:";
+ XCTAssertTrue([FIRApp validateAppIDFormat:kGoodAppIDV1 withVersion:kGoodVersionV1]);
+
+ NSString *const kGoodAppIDV2 = @"2:1337:ios:5e18052ab54fbfec";
+ NSString *const kGoodVersionV2 = @"2:";
+ XCTAssertTrue([FIRApp validateAppIDFormat:kGoodAppIDV2 withVersion:kGoodVersionV2]);
+
+ // Version mismatch.
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodAppIDV2 withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodAppIDV1 withVersion:kGoodVersionV2]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodAppIDV1 withVersion:@"999:"]);
+
+ // Nil or empty strings.
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodAppIDV1 withVersion:nil]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodAppIDV1 withVersion:@""]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:nil withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:nil withVersion:nil]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"" withVersion:@""]);
+
+ // App ID contains only the version prefix.
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodVersionV1 withVersion:kGoodVersionV1]);
+ // The version is the entire app ID.
+ XCTAssertFalse([FIRApp validateAppIDFormat:kGoodAppIDV1 withVersion:kGoodAppIDV1]);
+
+ // Versions digits that may make a partial match.
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"01:1337:ios:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"10:1337:ios:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"11:1337:ios:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"21:1337:ios:5e18052ab54fbfec"
+ withVersion:kGoodVersionV2]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"22:1337:ios:5e18052ab54fbfec"
+ withVersion:kGoodVersionV2]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"02:1337:ios:5e18052ab54fbfec"
+ withVersion:kGoodVersionV2]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"20:1337:ios:5e18052ab54fbfec"
+ withVersion:kGoodVersionV2]);
+
+ // Extra fields.
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"ab:1:1337:ios:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"1:ab:1337:ios:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"1:1337:ab:ios:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"1:1337:ios:ab:deadbeef" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFormat:@"1:1337:ios:deadbeef:ab" withVersion:kGoodVersionV1]);
+}
+
+- (void)testAppIDFingerprintInvalid {
+ OCMStub([self.appClassMock actualBundleID]).andReturn(@"com.google.bundleID");
+ // Some direct tests of the validateAppIDFingerprint:withVersion: method.
+ // Sanity checks first.
+ NSString *const kGoodAppIDV1 = @"1:1337:ios:deadbeef";
+ NSString *const kGoodVersionV1 = @"1:";
+ XCTAssertTrue([FIRApp validateAppIDFingerprint:kGoodAppIDV1 withVersion:kGoodVersionV1]);
+
+ NSString *const kGoodAppIDV2 = @"2:1337:ios:5e18052ab54fbfec";
+ NSString *const kGoodVersionV2 = @"2:";
+ XCTAssertTrue([FIRApp validateAppIDFormat:kGoodAppIDV2 withVersion:kGoodVersionV2]);
+
+ // Version mismatch.
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodAppIDV2 withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodAppIDV1 withVersion:kGoodVersionV2]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodAppIDV1 withVersion:@"999:"]);
+
+ // Nil or empty strings.
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodAppIDV1 withVersion:nil]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodAppIDV1 withVersion:@""]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:nil withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"" withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:nil withVersion:nil]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"" withVersion:@""]);
+
+ // App ID contains only the version prefix.
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodVersionV1 withVersion:kGoodVersionV1]);
+ // The version is the entire app ID.
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodAppIDV1 withVersion:kGoodAppIDV1]);
+
+ // Versions digits that may make a partial match.
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"01:1337:ios:deadbeef"
+ withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"10:1337:ios:deadbeef"
+ withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"11:1337:ios:deadbeef"
+ withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"21:1337:ios:5e18052ab54fbfec"
+ withVersion:kGoodVersionV2]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"22:1337:ios:5e18052ab54fbfec"
+ withVersion:kGoodVersionV2]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"02:1337:ios:5e18052ab54fbfec"
+ withVersion:kGoodVersionV2]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"20:1337:ios:5e18052ab54fbfec"
+ withVersion:kGoodVersionV2]);
+ // Extra fields.
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"ab:1:1337:ios:deadbeef"
+ withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"1:ab:1337:ios:deadbeef"
+ withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"1:1337:ab:ios:deadbeef"
+ withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"1:1337:ios:ab:deadbeef"
+ withVersion:kGoodVersionV1]);
+ XCTAssertFalse([FIRApp validateAppIDFingerprint:@"1:1337:ios:deadbeef:ab"
+ withVersion:kGoodVersionV1]);
+}
+
+#pragma mark - Internal Methods
+
+- (void)testAuthGetUID {
+ [FIRApp configure];
+
+ [FIRApp defaultApp].getUIDImplementation = ^NSString *{ return @"highlander"; };
+ XCTAssertEqual([[FIRApp defaultApp] getUID], @"highlander");
+}
+
+#pragma mark - private
+
+- (NSDictionary <NSString *, NSObject *> *)expectedUserInfoWithAppName:(NSString *)name
+ isDefaultApp:(BOOL)isDefaultApp {
+ return @{
+ kFIRAppNameKey : name,
+ kFIRAppIsDefaultAppKey : [NSNumber numberWithBool:isDefaultApp],
+ kFIRGoogleAppIDKey : kGoogleAppID
+ };
+}
+
+@end
diff --git a/Example/Core/Tests/FIRBundleUtilTest.m b/Example/Core/Tests/FIRBundleUtilTest.m
new file mode 100644
index 0000000..6a3e20a
--- /dev/null
+++ b/Example/Core/Tests/FIRBundleUtilTest.m
@@ -0,0 +1,86 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRBundleUtil.h"
+
+#import "FIRTestCase.h"
+
+static NSString *const kResultPath = @"resultPath";
+static NSString *const kResourceName = @"resourceName";
+static NSString *const kFileType = @"fileType";
+
+@interface FIRBundleUtilTest : FIRTestCase
+
+@property(nonatomic, strong) id mockBundle;
+
+@end
+
+@implementation FIRBundleUtilTest
+
+- (void)setUp {
+ [super setUp];
+ self.mockBundle = OCMClassMock([NSBundle class]);
+}
+
+- (void)testRelevantBundles_mainIsFirst {
+ // Pointer compare to same instance of main bundle.
+ XCTAssertEqual([NSBundle mainBundle], [FIRBundleUtil relevantBundles][0]);
+}
+
+// TODO: test that adding a bundle appears in "all bundles"
+// once the use-case is understood.
+
+- (void)testFindOptionsDictionaryPath {
+ [OCMStub([self.mockBundle pathForResource:kResourceName ofType:kFileType]) andReturn:kResultPath];
+ XCTAssertEqualObjects(
+ [FIRBundleUtil optionsDictionaryPathWithResourceName:kResourceName
+ andFileType:kFileType
+ inBundles:@[ self.mockBundle ]],
+ kResultPath);
+}
+
+- (void)testFindOptionsDictionaryPath_notFound {
+ XCTAssertNil([FIRBundleUtil optionsDictionaryPathWithResourceName:kResourceName
+ andFileType:kFileType
+ inBundles:@[ self.mockBundle ]]);
+}
+
+- (void)testFindOptionsDictionaryPath_secondBundle {
+ NSBundle *mockBundleEmpty = OCMClassMock([NSBundle class]);
+ [OCMStub([self.mockBundle pathForResource:kResourceName ofType:kFileType]) andReturn:kResultPath];
+
+ NSArray *bundles = @[ mockBundleEmpty, self.mockBundle ];
+ XCTAssertEqualObjects(
+ [FIRBundleUtil optionsDictionaryPathWithResourceName:kResourceName
+ andFileType:kFileType
+ inBundles:bundles],
+ kResultPath);
+}
+
+- (void)testBundleIdentifierExistsInBundles {
+ NSString *bundleID = @"com.google.test";
+ [OCMStub([self.mockBundle bundleIdentifier]) andReturn:bundleID];
+ XCTAssertTrue([FIRBundleUtil hasBundleIdentifier:bundleID inBundles:@[ self.mockBundle ]]);
+}
+
+- (void)testBundleIdentifierExistsInBundles_notExist {
+ [OCMStub([self.mockBundle bundleIdentifier]) andReturn:@"com.google.test"];
+ XCTAssertFalse([FIRBundleUtil hasBundleIdentifier:@"not-exist" inBundles:@[ self.mockBundle ]]);
+}
+
+- (void)testBundleIdentifierExistsInBundles_emptyBundlesArray {
+ XCTAssertFalse([FIRBundleUtil hasBundleIdentifier:@"com.google.test" inBundles:@[ ]]);
+}
+
+@end
diff --git a/Example/Core/Tests/FIRConfigurationTest.m b/Example/Core/Tests/FIRConfigurationTest.m
new file mode 100644
index 0000000..2b3ff46
--- /dev/null
+++ b/Example/Core/Tests/FIRConfigurationTest.m
@@ -0,0 +1,31 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRConfiguration.h"
+
+#import "FIRTestCase.h"
+
+@interface FIRConfigurationTest : FIRTestCase
+
+@end
+
+@implementation FIRConfigurationTest
+
+- (void)testSharedInstance {
+ FIRConfiguration *config = [FIRConfiguration sharedInstance];
+ XCTAssertNotNil(config);
+ XCTAssertNotNil(config.analyticsConfiguration);
+}
+
+@end
diff --git a/Example/Core/Tests/FIRLoggerTest.m b/Example/Core/Tests/FIRLoggerTest.m
new file mode 100644
index 0000000..e7031a7
--- /dev/null
+++ b/Example/Core/Tests/FIRLoggerTest.m
@@ -0,0 +1,265 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRLogger.h"
+#import "FIRTestCase.h"
+
+#import <asl.h>
+
+// The following constants are exposed from FIRLogger for unit tests.
+extern NSString *const kFIRDisableDebugModeApplicationArgument;
+extern NSString *const kFIREnableDebugModeApplicationArgument;
+
+extern NSString *const kFIRPersistedDebugModeKey;
+
+extern const char *kFIRLoggerASLClientFacilityName;
+
+extern const char *kFIRLoggerCustomASLMessageFormat;
+
+extern void FIRResetLogger();
+
+extern aslclient getFIRLoggerClient();
+
+extern dispatch_queue_t getFIRClientQueue();
+
+extern BOOL getFIRLoggerDebugMode();
+
+// Define the message format again to make sure the format doesn't accidentally change.
+static NSString *const kCorrectASLMessageFormat =
+ @"$((Time)(J.3)) $(Sender)[$(PID)] <$((Level)(str))> $Message";
+
+static NSString *const kMessageCode = @"I-COR000001";
+
+@interface FIRLoggerTest : FIRTestCase
+
+@property(nonatomic) NSString *randomLogString;
+
+@end
+
+@implementation FIRLoggerTest
+
+- (void)setUp {
+ [super setUp];
+ FIRResetLogger();
+}
+
+// Test some stable variables to make sure they weren't accidently changed.
+- (void)testStableVariables {
+ // kFIRLoggerCustomASLMessageFormat.
+ XCTAssertEqualObjects(kCorrectASLMessageFormat,
+ [NSString stringWithUTF8String:kFIRLoggerCustomASLMessageFormat]);
+
+ // Strings of type FIRLoggerServices.
+ XCTAssertEqualObjects(kFIRLoggerABTesting, @"[Firebase/ABTesting]");
+ XCTAssertEqualObjects(kFIRLoggerAdMob, @"[Firebase/AdMob]");
+ XCTAssertEqualObjects(kFIRLoggerAnalytics, @"[Firebase/Analytics]");
+ XCTAssertEqualObjects(kFIRLoggerAuth, @"[Firebase/Auth]");
+ XCTAssertEqualObjects(kFIRLoggerCore, @"[Firebase/Core]");
+ XCTAssertEqualObjects(kFIRLoggerCrash, @"[Firebase/Crash]");
+ XCTAssertEqualObjects(kFIRLoggerDatabase, @"[Firebase/Database]");
+ XCTAssertEqualObjects(kFIRLoggerDynamicLinks, @"[Firebase/DynamicLinks]");
+ XCTAssertEqualObjects(kFIRLoggerInstanceID, @"[Firebase/InstanceID]");
+ XCTAssertEqualObjects(kFIRLoggerInvites, @"[Firebase/Invites]");
+ XCTAssertEqualObjects(kFIRLoggerMessaging, @"[Firebase/Messaging]");
+ XCTAssertEqualObjects(kFIRLoggerRemoteConfig, @"[Firebase/RemoteConfig]");
+ XCTAssertEqualObjects(kFIRLoggerStorage, @"[Firebase/Storage]");
+}
+
+- (void)testInitializeASLForNonDebugMode {
+ // Stub.
+ id processInfoMock = [OCMockObject partialMockForObject:[NSProcessInfo processInfo]];
+ NSArray *arguments = @[ kFIRDisableDebugModeApplicationArgument ];
+ [[[processInfoMock stub] andReturn:arguments] arguments];
+
+ // Test.
+ FIRLogError(kFIRLoggerCore, kMessageCode, @"Some error.");
+
+ // Assert.
+ NSNumber *debugMode =
+ [[NSUserDefaults standardUserDefaults] objectForKey:kFIRPersistedDebugModeKey];
+ XCTAssertNil(debugMode);
+ XCTAssertFalse(getFIRLoggerDebugMode());
+
+ // Stop.
+ [processInfoMock stopMocking];
+}
+
+- (void)testInitializeASLForDebugModeWithArgument {
+ // Stub.
+ id processInfoMock = [OCMockObject partialMockForObject:[NSProcessInfo processInfo]];
+ NSArray *arguments = @[ kFIREnableDebugModeApplicationArgument ];
+ [[[processInfoMock stub] andReturn:arguments] arguments];
+
+ // Test.
+ FIRLogError(kFIRLoggerCore, kMessageCode, @"Some error.");
+
+ // Assert.
+ NSNumber *debugMode =
+ [[NSUserDefaults standardUserDefaults] objectForKey:kFIRPersistedDebugModeKey];
+ XCTAssertTrue(debugMode.boolValue);
+ XCTAssertTrue(getFIRLoggerDebugMode());
+
+ // Stop.
+ [processInfoMock stopMocking];
+}
+
+- (void)testInitializeASLForDebugModeWithUserDefaults {
+ // Stub.
+ id userDefaultsMock = [OCMockObject partialMockForObject:[NSUserDefaults standardUserDefaults]];
+ NSNumber *debugMode = @YES;
+ [[[userDefaultsMock stub] andReturnValue:debugMode] boolForKey:kFIRPersistedDebugModeKey];
+
+ // Test.
+ FIRLogError(kFIRLoggerCore, kMessageCode, @"Some error.");
+
+ // Assert.
+ debugMode = [[NSUserDefaults standardUserDefaults] objectForKey:kFIRPersistedDebugModeKey];
+ XCTAssertTrue(debugMode.boolValue);
+ XCTAssertTrue(getFIRLoggerDebugMode());
+
+ // Stop.
+ [userDefaultsMock stopMocking];
+}
+
+- (void)testMessageCodeFormat {
+ // Valid case.
+ XCTAssertNoThrow(FIRLogError(kFIRLoggerCore, @"I-APP000001", @"Message."));
+
+ // An extra dash or missing dash should fail.
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"I-APP-000001", @"Message."));
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"IAPP000001", @"Message."));
+
+ // Wrong number of digits should fail.
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"I-APP00001", @"Message."));
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"I-APP0000001", @"Message."));
+
+ // Lowercase should fail.
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"I-app000001", @"Message."));
+
+ // nil or empty message code should fail.
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, nil, @"Message."));
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"", @"Message."));
+
+ // Android message code should fail.
+ XCTAssertThrows(FIRLogError(kFIRLoggerCore, @"A-APP000001", @"Message."));
+}
+
+
+- (void)testLoggerInterface {
+ XCTAssertNoThrow(FIRLogError(kFIRLoggerCore, kMessageCode, @"Message."));
+ XCTAssertNoThrow(FIRLogError(kFIRLoggerCore, kMessageCode, @"Configure %@.", @"blah"));
+
+ XCTAssertNoThrow(FIRLogWarning(kFIRLoggerCore, kMessageCode, @"Message."));
+ XCTAssertNoThrow(FIRLogWarning(kFIRLoggerCore, kMessageCode, @"Configure %@.", @"blah"));
+
+ XCTAssertNoThrow(FIRLogNotice(kFIRLoggerCore, kMessageCode, @"Message."));
+ XCTAssertNoThrow(FIRLogNotice(kFIRLoggerCore, kMessageCode, @"Configure %@.", @"blah"));
+
+ XCTAssertNoThrow(FIRLogInfo(kFIRLoggerCore, kMessageCode, @"Message."));
+ XCTAssertNoThrow(FIRLogInfo(kFIRLoggerCore, kMessageCode, @"Configure %@.", @"blah"));
+
+ XCTAssertNoThrow(FIRLogDebug(kFIRLoggerCore, kMessageCode, @"Message."));
+ XCTAssertNoThrow(FIRLogDebug(kFIRLoggerCore, kMessageCode, @"Configure %@.", @"blah"));
+}
+
+
+// asl_set_filter does not perform as expected in unit test environment with simulator. The
+// following test only checks whether the logs have been sent to system with the default settings in
+// the unit test environment.
+- (void)testSystemLogWithDefaultStatus {
+#if !(TARGET_OS_SIMULATOR)
+ // Test fails on device - b/38130372
+ return;
+#else
+ // Sets the time interval that we need to wait in order to fetch all the logs.
+ NSTimeInterval timeInterval = 0.1f;
+ // Generates a random string each time and check whether it has been logged.
+ // Log messages with Notice level and below should be logged to system/device by default.
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ FIRLogError(kFIRLoggerCore, kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertTrue([self logExists]);
+
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ FIRLogWarning(kFIRLoggerCore, kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertTrue([self logExists]);
+
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ FIRLogNotice(kFIRLoggerCore, kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertTrue([self logExists]);
+
+ // Log messages with Info level and above should NOT be logged to system/device by default.
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ FIRLogInfo(kFIRLoggerCore, kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertFalse([self logExists]);
+
+ self.randomLogString = [NSUUID UUID].UUIDString;
+ FIRLogDebug(kFIRLoggerCore, kMessageCode, @"%@", self.randomLogString);
+ [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:timeInterval]];
+ XCTAssertFalse([self logExists]);
+#endif
+}
+
+// The FIRLoggerLevel enum must match the ASL_LEVEL_* constants, but we manually redefine
+// them in FIRLoggerLevel.h since we cannot include <asl.h> (see b/34976089 for more details).
+// This test ensures the constants match.
+- (void)testFIRLoggerLevelValues {
+ XCTAssertEqual(FIRLoggerLevelError, ASL_LEVEL_ERR);
+ XCTAssertEqual(FIRLoggerLevelWarning, ASL_LEVEL_WARNING);
+ XCTAssertEqual(FIRLoggerLevelNotice, ASL_LEVEL_NOTICE);
+ XCTAssertEqual(FIRLoggerLevelInfo, ASL_LEVEL_INFO);
+ XCTAssertEqual(FIRLoggerLevelDebug, ASL_LEVEL_DEBUG);
+}
+
+
+// Helper functions.
+- (BOOL)logExists {
+ [self drainFIRClientQueue];
+ NSString *correctMsg = [NSString stringWithFormat:@"%@[%@] %@", kFIRLoggerCore, kMessageCode,
+ self.randomLogString];
+ return [self messageWasLogged:correctMsg];
+}
+
+
+- (void)drainFIRClientQueue {
+ dispatch_semaphore_t workerSemaphore = dispatch_semaphore_create(0);
+ dispatch_async(getFIRClientQueue(), ^{
+ dispatch_semaphore_signal(workerSemaphore);
+ });
+ dispatch_semaphore_wait(workerSemaphore, DISPATCH_TIME_FOREVER);
+}
+
+- (BOOL)messageWasLogged:(NSString *)message {
+ aslmsg query = asl_new(ASL_TYPE_QUERY);
+ asl_set_query(query, ASL_KEY_FACILITY, kFIRLoggerASLClientFacilityName, ASL_QUERY_OP_EQUAL);
+ aslresponse r = asl_search(getFIRLoggerClient(), query);
+ asl_free(query);
+ aslmsg m;
+ const char *val;
+ NSMutableArray *allMsg = [[NSMutableArray alloc] init];
+ while ((m = asl_next(r)) != NULL) {
+ val = asl_get(m, ASL_KEY_MSG);
+ if (val) {
+ [allMsg addObject:[NSString stringWithUTF8String:val]];
+ }
+ }
+ asl_free(m);
+ asl_release(r);
+ return [allMsg containsObject:message];
+}
+
+@end
diff --git a/Example/Core/Tests/FIROptionsTest.m b/Example/Core/Tests/FIROptionsTest.m
new file mode 100644
index 0000000..62b6294
--- /dev/null
+++ b/Example/Core/Tests/FIROptionsTest.m
@@ -0,0 +1,468 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRAppInternal.h"
+#import "FIRBundleUtil.h"
+#import "FIROptionsInternal.h"
+
+#import "FIRTestCase.h"
+
+extern NSString *const kFIRIsMeasurementEnabled;
+extern NSString *const kFIRIsAnalyticsCollectionEnabled;
+extern NSString *const kFIRIsAnalyticsCollectionDeactivated;
+extern NSString *const kFIRLibraryVersionID;
+
+@interface FIROptions (Test)
+
+@property(nonatomic, readonly) NSDictionary *analyticsOptionsDictionary;
+
+@end
+
+@interface FIROptionsTest : FIRTestCase
+
+@end
+
+@implementation FIROptionsTest
+
+- (void)setUp {
+ [super setUp];
+ [FIROptions resetDefaultOptions];
+}
+
+- (void)testInit {
+ NSDictionary *optionsDictionary = [FIROptions defaultOptionsDictionary];
+ FIROptions *options =
+ [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ [self assertOptionsMatchDefaults:options andProjectID:YES];
+ XCTAssertNil(options.deepLinkURLScheme);
+ XCTAssertTrue(options.usingOptionsFromDefaultPlist);
+
+ options.deepLinkURLScheme = kDeepLinkURLScheme;
+ XCTAssertEqualObjects(options.deepLinkURLScheme, kDeepLinkURLScheme);
+}
+
+- (void)testDefaultOptionsDictionaryWithNilFilePath {
+ id mockBundleUtil = OCMClassMock([FIRBundleUtil class]);
+ [OCMStub([mockBundleUtil optionsDictionaryPathWithResourceName:kServiceInfoFileName
+ andFileType:kServiceInfoFileType
+ inBundles:[FIRBundleUtil relevantBundles]])
+ andReturn:nil];
+ XCTAssertNil([FIROptions defaultOptionsDictionary]);
+}
+
+- (void)testDefaultOptionsDictionaryWithInvalidSourceFile {
+ id mockBundleUtil = OCMClassMock([FIRBundleUtil class]);
+ [OCMStub([mockBundleUtil optionsDictionaryPathWithResourceName:kServiceInfoFileName
+ andFileType:kServiceInfoFileType
+ inBundles:[FIRBundleUtil relevantBundles]])
+ andReturn:@"invalid.plist"];
+ XCTAssertNil([FIROptions defaultOptionsDictionary]);
+}
+
+- (void)testDefaultOptions {
+ FIROptions *options = [FIROptions defaultOptions];
+ [self assertOptionsMatchDefaults:options andProjectID:YES];
+ XCTAssertNil(options.deepLinkURLScheme);
+ XCTAssertTrue(options.usingOptionsFromDefaultPlist);
+
+ options.deepLinkURLScheme = kDeepLinkURLScheme;
+ XCTAssertEqualObjects(options.deepLinkURLScheme, kDeepLinkURLScheme);
+}
+
+- (void)testInitCustomizedOptions {
+ FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ bundleID:kBundleID
+ GCMSenderID:kGCMSenderID
+ APIKey:kAPIKey
+ clientID:kClientID
+ trackingID:kTrackingID
+ androidClientID:kAndroidClientID
+ databaseURL:kDatabaseURL
+ storageBucket:kStorageBucket
+ deepLinkURLScheme:kDeepLinkURLScheme];
+ [self assertOptionsMatchDefaults:options andProjectID:NO];
+ XCTAssertEqualObjects(options.deepLinkURLScheme, kDeepLinkURLScheme);
+ XCTAssertFalse(options.usingOptionsFromDefaultPlist);
+
+ FIROptions *options2 = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ GCMSenderID:kGCMSenderID];
+ options2.androidClientID = kAndroidClientID;
+ options2.APIKey = kAPIKey;
+ options2.bundleID = kBundleID;
+ options2.clientID = kClientID;
+ options2.databaseURL = kDatabaseURL;
+ options2.deepLinkURLScheme = kDeepLinkURLScheme;
+ options2.projectID = kProjectID;
+ options2.storageBucket = kStorageBucket;
+ options2.trackingID = kTrackingID;
+ [self assertOptionsMatchDefaults:options2 andProjectID:YES];
+ XCTAssertEqualObjects(options2.deepLinkURLScheme, kDeepLinkURLScheme);
+ XCTAssertFalse(options.usingOptionsFromDefaultPlist);
+
+ // nil GoogleAppID should throw an exception
+ XCTAssertThrows([[FIROptions alloc] initWithGoogleAppID:nil
+ bundleID:kBundleID
+ GCMSenderID:kGCMSenderID
+ APIKey:kCustomizedAPIKey
+ clientID:nil
+ trackingID:nil
+ androidClientID:nil
+ databaseURL:nil
+ storageBucket:nil
+ deepLinkURLScheme:nil]);
+}
+
+- (void)testinitWithContentsOfFile {
+ NSString *filePath =
+ [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"];
+ FIROptions *options = [[FIROptions alloc] initWithContentsOfFile:filePath];
+ [self assertOptionsMatchDefaults:options andProjectID:YES];
+ XCTAssertNil(options.deepLinkURLScheme);
+ XCTAssertFalse(options.usingOptionsFromDefaultPlist);
+
+ FIROptions *emptyOptions = [[FIROptions alloc] initWithContentsOfFile:nil];
+ XCTAssertNil(emptyOptions);
+
+ FIROptions *invalidOptions = [[FIROptions alloc] initWithContentsOfFile:@"invalid.plist"];
+ XCTAssertNil(invalidOptions);
+}
+
+- (void)assertOptionsMatchDefaults:(FIROptions *)options andProjectID:(BOOL)matchProjectID {
+ XCTAssertEqualObjects(options.googleAppID, kGoogleAppID);
+ XCTAssertEqualObjects(options.APIKey, kAPIKey);
+ XCTAssertEqualObjects(options.clientID, kClientID);
+ XCTAssertEqualObjects(options.trackingID, kTrackingID);
+ XCTAssertEqualObjects(options.GCMSenderID, kGCMSenderID);
+ XCTAssertEqualObjects(options.androidClientID, kAndroidClientID);
+ XCTAssertEqualObjects(options.libraryVersionID, kFIRLibraryVersionID);
+ XCTAssertEqualObjects(options.databaseURL, kDatabaseURL);
+ XCTAssertEqualObjects(options.storageBucket, kStorageBucket);
+ XCTAssertEqualObjects(options.bundleID, kBundleID);
+
+ // Custom `matchProjectID` parameter to be removed once the deprecated `FIROptions` constructor is
+ // removed.
+ if (matchProjectID) {
+ XCTAssertEqualObjects(options.projectID, kProjectID);
+ }
+}
+
+- (void)testCopyingProperties {
+ NSMutableString *mutableString;
+ FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ GCMSenderID:kGCMSenderID];
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.APIKey = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.APIKey, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.bundleID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.bundleID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.clientID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.clientID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.trackingID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.trackingID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.GCMSenderID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.GCMSenderID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.projectID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.projectID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.androidClientID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.androidClientID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.googleAppID = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.googleAppID, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.databaseURL = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.databaseURL, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.deepLinkURLScheme = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.deepLinkURLScheme, @"1");
+
+ mutableString = [[NSMutableString alloc] initWithString:@"1"];
+ options.storageBucket = mutableString;
+ [mutableString appendString:@"2"];
+ XCTAssertEqualObjects(options.storageBucket, @"1");
+}
+
+- (void)testCopyWithZone {
+ // default options
+ FIROptions *options = [FIROptions defaultOptions];
+ options.deepLinkURLScheme = kDeepLinkURLScheme;
+ XCTAssertEqualObjects(options.deepLinkURLScheme, kDeepLinkURLScheme);
+
+ FIROptions *newOptions = [options copy];
+ XCTAssertEqualObjects(newOptions.deepLinkURLScheme, kDeepLinkURLScheme);
+
+ [options setDeepLinkURLScheme:kNewDeepLinkURLScheme];
+ XCTAssertEqualObjects(options.deepLinkURLScheme, kNewDeepLinkURLScheme);
+ XCTAssertEqualObjects(newOptions.deepLinkURLScheme, kDeepLinkURLScheme);
+
+ // customized options
+ FIROptions *customizedOptions = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
+ bundleID:kBundleID
+ GCMSenderID:kGCMSenderID
+ APIKey:kAPIKey
+ clientID:kClientID
+ trackingID:kTrackingID
+ androidClientID:kAndroidClientID
+ databaseURL:kDatabaseURL
+ storageBucket:kStorageBucket
+ deepLinkURLScheme:kDeepLinkURLScheme];
+ FIROptions *copyCustomizedOptions = [customizedOptions copy];
+ [copyCustomizedOptions setDeepLinkURLScheme:kNewDeepLinkURLScheme];
+ XCTAssertEqualObjects(customizedOptions.deepLinkURLScheme, kDeepLinkURLScheme);
+ XCTAssertEqualObjects(copyCustomizedOptions.deepLinkURLScheme, kNewDeepLinkURLScheme);
+}
+
+- (void)testAnalyticsConstants {
+ // The keys are public values and should never change.
+ XCTAssertEqualObjects(kFIRIsMeasurementEnabled, @"IS_MEASUREMENT_ENABLED");
+ XCTAssertEqualObjects(kFIRIsAnalyticsCollectionEnabled, @"FIREBASE_ANALYTICS_COLLECTION_ENABLED");
+ XCTAssertEqualObjects(kFIRIsAnalyticsCollectionDeactivated,
+ @"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED");
+}
+
+- (void)testAnalyticsOptions {
+ id mainBundleMock = OCMPartialMock([NSBundle mainBundle]);
+
+ // No keys anywhere.
+ NSDictionary *optionsDictionary = nil;
+ FIROptions *options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ NSDictionary *mainDictionary = nil;
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ NSDictionary *expectedAnalyticsOptions = @{};
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ optionsDictionary = @{};
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{};
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = @{};
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ // Main has no keys.
+ optionsDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{};
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = optionsDictionary;
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ // Main overrides all the keys.
+ optionsDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @NO,
+ kFIRIsAnalyticsCollectionEnabled : @NO,
+ kFIRIsMeasurementEnabled : @NO
+ };
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = mainDictionary;
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ // Keys exist only in main.
+ optionsDictionary = @{};
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = mainDictionary;
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ // Main overrides single keys.
+ optionsDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{ kFIRIsAnalyticsCollectionDeactivated : @NO };
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = @{
+ kFIRIsAnalyticsCollectionDeactivated : @NO, // override
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ optionsDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{ kFIRIsAnalyticsCollectionEnabled : @NO };
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @NO, // override
+ kFIRIsMeasurementEnabled : @YES
+ };
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ optionsDictionary = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @YES
+ };
+ options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ mainDictionary = @{ kFIRIsMeasurementEnabled : @NO };
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ expectedAnalyticsOptions = @{
+ kFIRIsAnalyticsCollectionDeactivated : @YES,
+ kFIRIsAnalyticsCollectionEnabled : @YES,
+ kFIRIsMeasurementEnabled : @NO // override
+ };
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+}
+
+- (void)testAnalyticsOptions_combinatorial {
+ // Complete combinatorial test.
+ id mainBundleMock = OCMPartialMock([NSBundle mainBundle]);
+ // Possible values for the flags in the plist, where NSNull means the flag is not present.
+ NSArray *values = @[ [NSNull null], @NO, @YES ];
+
+ // Sanity checks for the combination generation.
+ int combinationCount = 0;
+ NSMutableArray *uniqueMainCombinations = [[NSMutableArray alloc] init];
+ NSMutableArray *uniqueOptionsCombinations = [[NSMutableArray alloc] init];
+
+ // Generate all optout flag combinations for { main plist X GoogleService-info options plist }.
+ // Options present in the main plist should override options of the same key in the service plist.
+ for (id mainDeactivated in values) {
+ for (id mainAnalyticsEnabled in values) {
+ for (id mainMeasurementEnabled in values) {
+ for (id optionsDeactivated in values) {
+ for (id optionsAnalyticsEnabled in values) {
+ for (id optionsMeasurementEnabled in values) {
+ @autoreleasepool {
+ // Fill the GoogleService-info options plist dictionary.
+ NSMutableDictionary *optionsDictionary = [[NSMutableDictionary alloc] init];
+ if (![optionsDeactivated isEqual:[NSNull null]]) {
+ optionsDictionary[kFIRIsAnalyticsCollectionDeactivated] = optionsDeactivated;
+ }
+ if (![optionsAnalyticsEnabled isEqual:[NSNull null]]) {
+ optionsDictionary[kFIRIsAnalyticsCollectionEnabled] = optionsAnalyticsEnabled;
+ }
+ if (![optionsMeasurementEnabled isEqual:[NSNull null]]) {
+ optionsDictionary[kFIRIsMeasurementEnabled] = optionsMeasurementEnabled;
+ }
+ FIROptions *options =
+ [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary];
+ if (![uniqueOptionsCombinations containsObject:optionsDictionary]) {
+ [uniqueOptionsCombinations addObject:optionsDictionary];
+ }
+
+ // Fill the main plist dictionary.
+ NSMutableDictionary *mainDictionary = [[NSMutableDictionary alloc] init];
+ if (![mainDeactivated isEqual:[NSNull null]]) {
+ mainDictionary[kFIRIsAnalyticsCollectionDeactivated] = mainDeactivated;
+ }
+ if (![mainAnalyticsEnabled isEqual:[NSNull null]]) {
+ mainDictionary[kFIRIsAnalyticsCollectionEnabled] = mainAnalyticsEnabled;
+ }
+ if (![mainMeasurementEnabled isEqual:[NSNull null]]) {
+ mainDictionary[kFIRIsMeasurementEnabled] = mainMeasurementEnabled;
+ }
+ OCMExpect([mainBundleMock infoDictionary]).andReturn(mainDictionary);
+ if (![uniqueMainCombinations containsObject:mainDictionary]) {
+ [uniqueMainCombinations addObject:mainDictionary];
+ }
+
+ // Generate the expected options by adding main values on top of the service options
+ // values. The main values will replace any existing options values with the same
+ // key. This is a different way of combining the two sets of flags from the actual
+ // implementation in FIROptions, with equivalent output.
+ NSMutableDictionary *expectedAnalyticsOptions =
+ [[NSMutableDictionary alloc] initWithDictionary:optionsDictionary];
+ [expectedAnalyticsOptions addEntriesFromDictionary:mainDictionary];
+ XCTAssertEqualObjects(options.analyticsOptionsDictionary, expectedAnalyticsOptions);
+
+ combinationCount++;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Verify the sanity checks.
+ XCTAssertEqual(combinationCount, 729); // = 3^6.
+
+ XCTAssertEqual(uniqueOptionsCombinations.count, 27);
+ int optionsSizeCount[4] = {0};
+ for (NSDictionary *dictionary in uniqueOptionsCombinations) {
+ optionsSizeCount[dictionary.count]++;
+ }
+ XCTAssertEqual(optionsSizeCount[0], 1);
+ XCTAssertEqual(optionsSizeCount[1], 6);
+ XCTAssertEqual(optionsSizeCount[2], 12);
+ XCTAssertEqual(optionsSizeCount[3], 8);
+
+ XCTAssertEqual(uniqueMainCombinations.count, 27);
+ int mainSizeCount[4] = {0};
+ for (NSDictionary *dictionary in uniqueMainCombinations) {
+ mainSizeCount[dictionary.count]++;
+ }
+ XCTAssertEqual(mainSizeCount[0], 1);
+ XCTAssertEqual(mainSizeCount[1], 6);
+ XCTAssertEqual(mainSizeCount[2], 12);
+ XCTAssertEqual(mainSizeCount[3], 8);
+}
+
+- (void)testVersionFormat {
+ NSRegularExpression *sLibraryVersionRegex =
+ [NSRegularExpression regularExpressionWithPattern:@"^[0-9]{8,}$" options:0 error:NULL];
+ NSUInteger numberOfMatches =
+ [sLibraryVersionRegex numberOfMatchesInString:kFIRLibraryVersionID
+ options:0
+ range:NSMakeRange(0, kFIRLibraryVersionID.length)];
+ XCTAssertEqual(numberOfMatches, 1, @"Incorrect library version format.");
+}
+
+@end
diff --git a/Example/Core/Tests/FIRTestCase.h b/Example/Core/Tests/FIRTestCase.h
new file mode 100644
index 0000000..acc7b16
--- /dev/null
+++ b/Example/Core/Tests/FIRTestCase.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+extern NSString *const kAPIKey;
+extern NSString *const kCustomizedAPIKey;
+extern NSString *const kClientID;
+extern NSString *const kTrackingID;
+extern NSString *const kGCMSenderID;
+extern NSString *const kAndroidClientID;
+extern NSString *const kGoogleAppID;
+extern NSString *const kDatabaseURL;
+extern NSString *const kStorageBucket;
+
+extern NSString *const kDeepLinkURLScheme;
+extern NSString *const kNewDeepLinkURLScheme;
+
+extern NSString *const kBundleID;
+extern NSString *const kProjectID;
+
+/**
+ * Base test case for Firebase Core SDK tests.
+ */
+@interface FIRTestCase : XCTestCase
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Example/Core/Tests/FIRTestCase.m b/Example/Core/Tests/FIRTestCase.m
new file mode 100644
index 0000000..2c68b8d
--- /dev/null
+++ b/Example/Core/Tests/FIRTestCase.m
@@ -0,0 +1,47 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "FIRTestCase.h"
+
+NSString *const kAPIKey = @"correct_api_key";
+NSString *const kCustomizedAPIKey = @"customized_api_key";
+NSString *const kClientID = @"correct_client_id";
+NSString *const kTrackingID = @"correct_tracking_id";
+NSString *const kGCMSenderID = @"correct_gcm_sender_id";
+NSString *const kAndroidClientID = @"correct_android_client_id";
+NSString *const kGoogleAppID = @"1:123:ios:123abc";
+NSString *const kDatabaseURL = @"https://abc-xyz-123.firebaseio.com";
+NSString *const kStorageBucket = @"project-id-123.storage.firebase.com";
+
+NSString *const kDeepLinkURLScheme = @"comgoogledeeplinkurl";
+NSString *const kNewDeepLinkURLScheme = @"newdeeplinkurlfortest";
+
+NSString *const kBundleID = @"com.google.FirebaseSDKTests";
+NSString *const kProjectID = @"abc-xyz-123";
+
+@interface FIRTestCase ()
+
+@end
+
+@implementation FIRTestCase
+
+- (void)setUp {
+ [super setUp];
+}
+
+- (void)tearDown {
+ [super tearDown];
+}
+
+@end
diff --git a/Example/Core/Tests/Tests-Info.plist b/Example/Core/Tests/Tests-Info.plist
new file mode 100644
index 0000000..169b6f7
--- /dev/null
+++ b/Example/Core/Tests/Tests-Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>