Skip to content

Commit 1bc3255

Browse files
Firebase Installations: validation of FIROptions parameters (#4683)
* FIS: Throw exception on incomplete Firebase config. * API docs updated. * FirebaseInstallations bump minor version: 1.1.0 * Fix tests * Text fixed * Make exception message more informative * Fix Remote Config tests. * Tests updated to use GCMSenderID if projectID is not available. * Use `GCMSenderID` when `projectID` is not available. * ./scripts/style.sh * Comments * GCMSenderID and projectID validation updated. * Comment * FirebaseInstallations: version bump to 1.0.1. * FIS changelog * FirebaseInstallations: version bump to 1.1.0 * FIS and Core changelogs. * Changelogs remove GCMSenderID * Typo * Typo * fix link * Revert Core changelog * FIS changelog fix.
1 parent 9313d61 commit 1bc3255

File tree

14 files changed

+180
-10
lines changed

14 files changed

+180
-10
lines changed

Example/InstanceID/Tests/FIRInstanceIDTest.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ - (void)testSharedInstance {
156156
// The shared instance relies on the default app being configured. Configure it.
157157
FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
158158
GCMSenderID:kGCMSenderID];
159+
options.APIKey = @"api-key";
160+
options.projectID = @"project-id";
159161
[FIRApp configureWithName:kFIRDefaultAppName options:options];
160162
FIRInstanceID *instanceID = [FIRInstanceID instanceID];
161163
XCTAssertNotNil(instanceID);

Example/Messaging/Tests/FIRMessagingInstanceTest.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class FIRMessagingInstanceTest: XCTestCase {
2424
// This is an example of a functional test case.
2525
// Use XCTAssert and related functions to verify your tests produce the correct results.
2626
let options = FirebaseOptions(googleAppID: "1:123:ios:123abc", gcmSenderID: "valid-sender-id")
27+
options.apiKey = "api-key"
28+
options.projectID = "project-id"
2729
FirebaseApp.configure(options: options)
2830
let original = Messaging.messaging()
2931

FirebaseInstallations.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'FirebaseInstallations'
3-
s.version = '1.0.0'
3+
s.version = '1.1.0'
44
s.summary = 'Firebase Installations for iOS'
55

66
s.description = <<-DESC

FirebaseInstallations/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# v1.1.0 -- M62.1
2+
3+
- [changed] Throw an exception when there are missing required `FirebaseOptions` parameters (`APIKey`, `googleAppID`, and `projectID`). Please make sure your `GoogleServices-Info.plist` (or `FirebaseOptions` if you configure Firebase in code) is up to date. The file and settings can be downloaded from the [Firebase Console](https://console.firebase.google.com/). (#4683)
4+
15
# v1.0.0 -- M62
26

37
- [added] The Firebase Installations Service is an infrastructure service for Firebase services that creates unique identifiers and authentication tokens for Firebase clients (called "Firebase Installations") enabling Firebase Targeting, i.e. interoperation between Firebase services.

FirebaseInstallations/Source/Library/FIRInstallations.m

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#import "FIRInstallationsErrorUtil.h"
3535
#import "FIRInstallationsIDController.h"
3636
#import "FIRInstallationsItem.h"
37+
#import "FIRInstallationsLogger.h"
3738
#import "FIRInstallationsStoredAuthToken.h"
3839
#import "FIRInstallationsVersion.h"
3940

@@ -101,6 +102,7 @@ - (instancetype)initWithAppOptions:(FIROptions *)appOptions
101102
prefetchAuthToken:(BOOL)prefetchAuthToken {
102103
self = [super init];
103104
if (self) {
105+
[[self class] validateAppOptions:appOptions appName:appName];
104106
[[self class] assertCompatibleIIDVersion];
105107

106108
_appOptions = [appOptions copy];
@@ -117,12 +119,44 @@ - (instancetype)initWithAppOptions:(FIROptions *)appOptions
117119
return self;
118120
}
119121

122+
+ (void)validateAppOptions:(FIROptions *)appOptions appName:(NSString *)appName {
123+
NSMutableArray *missingFields = [NSMutableArray array];
124+
if (appName.length < 1) {
125+
[missingFields addObject:@"`FirebaseApp.name`"];
126+
}
127+
if (appOptions.APIKey.length < 1) {
128+
[missingFields addObject:@"`FirebaseOptions.APIKey`"];
129+
}
130+
if (appOptions.googleAppID.length < 1) {
131+
[missingFields addObject:@"`FirebaseOptions.googleAppID`"];
132+
}
133+
134+
// TODO(#4692): Check for `appOptions.projectID.length < 1` only.
135+
// We can use `GCMSenderID` instead of `projectID` temporary.
136+
if (appOptions.projectID.length < 1 && appOptions.GCMSenderID.length < 1) {
137+
[missingFields addObject:@"`FirebaseOptions.projectID`"];
138+
}
139+
140+
if (missingFields.count > 0) {
141+
[NSException
142+
raise:kFirebaseInstallationsErrorDomain
143+
format:
144+
@"%@[%@] Could not configure Firebase Installations due to invalid FirebaseApp "
145+
@"options. The following parameters are nil or empty: %@. If you use "
146+
@"GoogleServices-Info.plist please download the most recent version from the Firebase "
147+
@"Console. If you configure Firebase in code, please make sure you specify all "
148+
@"required parameters.",
149+
kFIRLoggerInstallations, kFIRInstallationsMessageCodeInvalidFirebaseAppOptions,
150+
[missingFields componentsJoinedByString:@", "]];
151+
}
152+
}
153+
120154
#pragma mark - Public
121155

122156
+ (FIRInstallations *)installations {
123157
FIRApp *defaultApp = [FIRApp defaultApp];
124158
if (!defaultApp) {
125-
[NSException raise:NSInternalInconsistencyException
159+
[NSException raise:kFirebaseInstallationsErrorDomain
126160
format:@"The default FirebaseApp instance must be configured before the default"
127161
@"FirebaseApp instance can be initialized. One way to ensure that is to "
128162
@"call `[FIRApp configure];` (`FirebaseApp.configure()` in Swift) in the App"
@@ -191,7 +225,7 @@ + (void)assertCompatibleIIDVersion {
191225
return;
192226
#else
193227
if (![self isIIDVersionCompatible]) {
194-
[NSException raise:NSInternalInconsistencyException
228+
[NSException raise:kFirebaseInstallationsErrorDomain
195229
format:@"FirebaseInstallations will not work correctly with current version of "
196230
@"Firebase Instance ID. Please update your Firebase Instance ID version."];
197231
}

FirebaseInstallations/Source/Library/FIRInstallationsLogger.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,6 @@ extern NSString *const kFIRInstallationsMessageCodeAuthTokenCoderVersionMismatch
4646
// FIRInstallationsStoredIIDCheckin.m
4747
extern NSString *const kFIRInstallationsMessageCodeIIDCheckinCoderVersionMismatch;
4848
extern NSString *const kFIRInstallationsMessageCodeIIDCheckinFailedToDecode;
49+
50+
// FIRInstallations.m
51+
extern NSString *const kFIRInstallationsMessageCodeInvalidFirebaseAppOptions;

FirebaseInstallations/Source/Library/FIRInstallationsLogger.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,6 @@
4444
// FIRInstallationsStoredIIDCheckin.m
4545
NSString *const kFIRInstallationsMessageCodeIIDCheckinCoderVersionMismatch = @"I-FIS007000";
4646
NSString *const kFIRInstallationsMessageCodeIIDCheckinFailedToDecode = @"I-FIS007001";
47+
48+
// FIRInstallations.m
49+
NSString *const kFIRInstallationsMessageCodeInvalidFirebaseAppOptions = @"I-FIS008000";

FirebaseInstallations/Source/Library/InstallationsIDController/FIRInstallationsIDController.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN
3131
APIKey:(NSString *)APIKey
3232
projectID:(NSString *)projectID
3333
GCMSenderID:(NSString *)GCMSenderID
34-
accessGroup:(NSString *)accessGroup;
34+
accessGroup:(nullable NSString *)accessGroup;
3535

3636
- (FBLPromise<FIRInstallationsItem *> *)getInstallationItem;
3737

FirebaseInstallations/Source/Library/InstallationsIDController/FIRInstallationsIDController.m

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,12 @@ - (instancetype)initWithGoogleAppID:(NSString *)appID
7575
FIRSecureStorage *secureStorage = [[FIRSecureStorage alloc] init];
7676
FIRInstallationsStore *installationsStore =
7777
[[FIRInstallationsStore alloc] initWithSecureStorage:secureStorage accessGroup:accessGroup];
78+
79+
// Use `GCMSenderID` as project identifier when `projectID` is not available.
80+
NSString *APIServiceProjectID = (projectID.length > 0) ? projectID : GCMSenderID;
7881
FIRInstallationsAPIService *apiService =
79-
[[FIRInstallationsAPIService alloc] initWithAPIKey:APIKey projectID:projectID];
82+
[[FIRInstallationsAPIService alloc] initWithAPIKey:APIKey projectID:APIServiceProjectID];
83+
8084
FIRInstallationsIIDStore *IIDStore = [[FIRInstallationsIIDStore alloc] init];
8185
FIRInstallationsIIDTokenStore *IIDCheckingStore =
8286
[[FIRInstallationsIIDTokenStore alloc] initWithGCMSenderID:GCMSenderID];

FirebaseInstallations/Source/Library/Public/FIRInstallations.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,17 @@ NS_SWIFT_NAME(Installations)
5959

6060
/**
6161
* Returns a default instance of `Installations`.
62-
* @return Returns an instance of `Installations` for `FirebaseApp.defaultApp(). Throws an exception
63-
* if the default app is not configured yet.
62+
* @returns An instance of `Installations` for `FirebaseApp.defaultApp().
63+
* @throw Throws an exception if the default app is not configured yet or required `FirebaseApp`
64+
* options are missing.
6465
*/
6566
+ (FIRInstallations *)installations NS_SWIFT_NAME(installations());
6667

6768
/**
6869
* Returns an instance of `Installations` for an application.
6970
* @param application A configured `FirebaseApp` instance.
70-
* @return Returns an instance of `Installations` corresponding to the passed application.
71+
* @returns An instance of `Installations` corresponding to the passed application.
72+
* @throw Throws an exception if required `FirebaseApp` options are missing.
7173
*/
7274
+ (FIRInstallations *)installationsWithApp:(FIRApp *)application NS_SWIFT_NAME(installations(app:));
7375

FirebaseInstallations/Source/Tests/Integration/FIRInstallationsIntegrationTests.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ - (FIRApp *)createAndConfigureAppWithName:(NSString *)name {
205205
FIROptions *options =
206206
[[FIROptions alloc] initWithGoogleAppID:@"1:100000000000:ios:aaaaaaaaaaaaaaaaaaaaaaaa"
207207
GCMSenderID:@"valid_sender_id"];
208+
options.APIKey = @"some_api_key";
209+
options.projectID = @"project_id";
208210
[FIRApp configureWithName:name options:options];
209211

210212
return [FIRApp appNamed:name];

FirebaseInstallations/Source/Tests/Unit/FIRInstallationsIDControllerTests.m

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,46 @@ - (void)tearDown {
8787
self.appName = nil;
8888
}
8989

90+
#pragma mark - Initialization
91+
92+
- (void)testInitWhenProjectIDSetThenItIsPassedToAPIService {
93+
NSString *APIKey = @"api-key";
94+
NSString *projectID = @"project-id";
95+
OCMExpect([self.mockAPIService alloc]).andReturn(self.mockAPIService);
96+
OCMExpect([self.mockAPIService initWithAPIKey:APIKey projectID:projectID])
97+
.andReturn(self.mockAPIService);
98+
99+
FIRInstallationsIDController *controller =
100+
[[FIRInstallationsIDController alloc] initWithGoogleAppID:@"app-id"
101+
appName:@"app-name"
102+
APIKey:APIKey
103+
projectID:projectID
104+
GCMSenderID:@"sender-id"
105+
accessGroup:nil];
106+
XCTAssertNotNil(controller);
107+
108+
OCMVerifyAll(self.mockAPIService);
109+
}
110+
111+
- (void)testInitWhenProjectIDIsNilThenGCMSenderIDIsPassedToAPIServiceAsProjectID {
112+
NSString *APIKey = @"api-key";
113+
NSString *GCMSenderID = @"sender-id";
114+
OCMExpect([self.mockAPIService alloc]).andReturn(self.mockAPIService);
115+
OCMExpect([self.mockAPIService initWithAPIKey:APIKey projectID:GCMSenderID])
116+
.andReturn(self.mockAPIService);
117+
118+
FIRInstallationsIDController *controller =
119+
[[FIRInstallationsIDController alloc] initWithGoogleAppID:@"app-id"
120+
appName:@"app-name"
121+
APIKey:APIKey
122+
projectID:@""
123+
GCMSenderID:GCMSenderID
124+
accessGroup:nil];
125+
XCTAssertNotNil(controller);
126+
127+
OCMVerifyAll(self.mockAPIService);
128+
}
129+
90130
#pragma mark - Get Installation
91131

92132
- (void)testGetInstallationItem_WhenFIDExists_ThenItIsReturned {
@@ -1134,4 +1174,8 @@ - (FIRInstallationsItem *)expectAuthTokenRefreshForInstallation:
11341174
return responseInstallation;
11351175
}
11361176

1177+
- (void)expectAPIServiceInitWithAPIKey:(NSString *)APIKey projectID:(NSString *)projectID {
1178+
OCMExpect([self.mockAPIService initWithAPIKey:APIKey projectID:projectID]);
1179+
}
1180+
11371181
@end

FirebaseInstallations/Source/Tests/Unit/FIRInstallationsTests.m

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ - (void)setUp {
4545

4646
self.appOptions = [[FIROptions alloc] initWithGoogleAppID:@"GoogleAppID"
4747
GCMSenderID:@"GCMSenderID"];
48+
self.appOptions.APIKey = @"APIKey";
49+
self.appOptions.projectID = @"ProjectID";
50+
4851
self.mockIDController = OCMClassMock([FIRInstallationsIDController class]);
4952
self.installations = [[FIRInstallations alloc] initWithAppOptions:self.appOptions
5053
appName:@"appName"
@@ -234,4 +237,68 @@ - (void)testDeleteError {
234237
[self waitForExpectations:@[ deleteExpectation ] timeout:0.5];
235238
}
236239

240+
#pragma mark - Invalid Firebase configuration
241+
242+
- (void)testInitWhenProjectIDMissingThenNoThrow {
243+
FIROptions *options = [self.appOptions copy];
244+
options.projectID = nil;
245+
XCTAssertNoThrow([self createInstallationsWithAppOptions:options appName:@"missingProjectID"]);
246+
247+
options.projectID = @"";
248+
XCTAssertNoThrow([self createInstallationsWithAppOptions:options appName:@"emptyProjectID"]);
249+
}
250+
251+
- (void)testInitWhenAPIKeyMissingThenThrows {
252+
FIROptions *options = [self.appOptions copy];
253+
options.APIKey = nil;
254+
XCTAssertThrows([self createInstallationsWithAppOptions:options appName:@"missingAPIKey"]);
255+
256+
options.APIKey = @"";
257+
XCTAssertThrows([self createInstallationsWithAppOptions:options appName:@"emptyAPIKey"]);
258+
}
259+
260+
- (void)testInitWhenGoogleAppIDMissingThenThrows {
261+
FIROptions *options = [self.appOptions copy];
262+
options.googleAppID = @"";
263+
XCTAssertThrows([self createInstallationsWithAppOptions:options appName:@"emptyGoogleAppID"]);
264+
}
265+
266+
- (void)testInitWhenGCMSenderIDMissingThenThrows {
267+
FIROptions *options = [self.appOptions copy];
268+
options.GCMSenderID = @"";
269+
XCTAssertNoThrow([self createInstallationsWithAppOptions:options appName:@"emptyGCMSenderID"]);
270+
}
271+
272+
- (void)testInitWhenProjectIDAndGCMSenderIDMissingThenNoThrow {
273+
FIROptions *options = [self.appOptions copy];
274+
options.GCMSenderID = @"";
275+
276+
options.projectID = nil;
277+
XCTAssertThrows([self createInstallationsWithAppOptions:options appName:@"missingProjectID"]);
278+
279+
options.projectID = @"";
280+
XCTAssertThrows([self createInstallationsWithAppOptions:options appName:@"emptyProjectID"]);
281+
}
282+
283+
- (void)testInitWhenAppNameMissingThenThrows {
284+
FIROptions *options = [self.appOptions copy];
285+
XCTAssertThrows([self createInstallationsWithAppOptions:options appName:@""]);
286+
XCTAssertThrows([self createInstallationsWithAppOptions:options appName:nil]);
287+
}
288+
289+
- (void)testInitWhenAppOptionsMissingThenThrows {
290+
XCTAssertThrows([self createInstallationsWithAppOptions:nil appName:@"missingOptions"]);
291+
}
292+
293+
#pragma mark - Helpers
294+
295+
- (FIRInstallations *)createInstallationsWithAppOptions:(FIROptions *)options
296+
appName:(NSString *)appName {
297+
id mockIDController = OCMClassMock([FIRInstallationsIDController class]);
298+
return [[FIRInstallations alloc] initWithAppOptions:options
299+
appName:appName
300+
installationsIDController:mockIDController
301+
prefetchAuthToken:NO];
302+
}
303+
237304
@end

FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,11 @@ - (void)testThrowsWithNilGCMSenderID {
173173
#pragma mark - Helpers
174174

175175
- (FIROptions *)fakeOptions {
176-
return [[FIROptions alloc] initWithGoogleAppID:@"1:123:ios:123abc"
177-
GCMSenderID:@"correct_gcm_sender_id"];
176+
FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"1:123:ios:123abc"
177+
GCMSenderID:@"correct_gcm_sender_id"];
178+
options.APIKey = @"api-key";
179+
options.projectID = @"project-id";
180+
return options;
178181
}
179182

180183
- (NSString *)generatedTestAppName {

0 commit comments

Comments
 (0)