Skip to content

Commit 2a43d21

Browse files
FirebaseInstallations: re-generate Installation on HTTP 401 and 404 (#4079)
* WIP: FID re-generating introduced. * WIP: FID regenerating tests. * FIS regenerate: tests and fixes. * Cleanup.
1 parent 5a39cf1 commit 2a43d21

File tree

6 files changed

+177
-24
lines changed

6 files changed

+177
-24
lines changed

FirebaseInstallations/Source/Library/Errors/FIRInstallationsHTTPError.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ NS_ASSUME_NONNULL_BEGIN
3232

3333
NS_ASSUME_NONNULL_END
3434

35+
typedef NS_ENUM(NSInteger, FIRInstallationsHTTPCodes) {
36+
FIRInstallationsHTTPCodesTooManyRequests = 429,
37+
FIRInstallationsHTTPCodesServerInternalError = 500,
38+
};
39+
3540
/** Possible response HTTP codes for `CreateInstallation` API request. */
3641
typedef NS_ENUM(NSInteger, FIRInstallationsRegistrationHTTPCode) {
3742
FIRInstallationsRegistrationHTTPCodeSuccess = 201,
@@ -43,4 +48,7 @@ typedef NS_ENUM(NSInteger, FIRInstallationsRegistrationHTTPCode) {
4348
FIRInstallationsRegistrationHTTPCodeServerInternalError = 500
4449
};
4550

46-
extern NSInteger const kFIRInstallationsAPIInternalErrorHTTPCode;
51+
typedef NS_ENUM(NSInteger, FIRInstallationsAuthTokenHTTPCode) {
52+
FIRInstallationsAuthTokenHTTPCodeInvalidAuthentication = 401,
53+
FIRInstallationsAuthTokenHTTPCodeFIDNotFound = 404,
54+
};

FirebaseInstallations/Source/Library/Errors/FIRInstallationsHTTPError.m

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,3 @@ + (BOOL)supportsSecureCoding {
7676
}
7777

7878
@end
79-
80-
NSInteger const kFIRInstallationsAPIInternalErrorHTTPCode = 500;

FirebaseInstallations/Source/Library/InstallationsIDController/FIRInstallationsIDController.m

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ - (FIRInstallationsStoredRegistrationParameters *)currentRegistrationParameters
325325
#pragma mark - Auth Token
326326

327327
- (FBLPromise<FIRInstallationsItem *> *)getAuthTokenForcingRefresh:(BOOL)forceRefresh {
328-
if (forceRefresh) {
328+
if (forceRefresh || [self.authTokenForcingRefreshPromiseCache getExistingPendingPromise] != nil) {
329329
return [self.authTokenForcingRefreshPromiseCache getExistingPendingOrCreateNewPromise];
330330
} else {
331331
return [self.authTokenPromiseCache getExistingPendingOrCreateNewPromise];
@@ -353,11 +353,37 @@ - (FIRInstallationsStoredRegistrationParameters *)currentRegistrationParameters
353353
return registeredInstallation;
354354
}
355355
})
356-
.catch(^void(NSError *error){
357-
// TODO: Handle the errors.
356+
.recover(^id(NSError *error) {
357+
return [self regenerateFIDOnRefreshTokenErrorIfNeeded:error];
358358
});
359359
}
360360

361+
- (id)regenerateFIDOnRefreshTokenErrorIfNeeded:(NSError *)error {
362+
if (![error isKindOfClass:[FIRInstallationsHTTPError class]]) {
363+
// No recovery possible. Return the same error.
364+
return error;
365+
}
366+
367+
FIRInstallationsHTTPError *HTTPError = (FIRInstallationsHTTPError *)error;
368+
switch (HTTPError.HTTPResponse.statusCode) {
369+
case FIRInstallationsAuthTokenHTTPCodeInvalidAuthentication:
370+
case FIRInstallationsAuthTokenHTTPCodeFIDNotFound:
371+
// The stored installation was damaged or blocked by the server.
372+
// Delete the stored installation then generate and register a new one.
373+
return [self getInstallationItem]
374+
.then(^FBLPromise<NSNull *> *(FIRInstallationsItem *installation) {
375+
return [self deleteInstallationLocally:installation];
376+
})
377+
.then(^FBLPromise<FIRInstallationsItem *> *(id result) {
378+
return [self installationWithValidAuthTokenForcingRefresh:NO];
379+
});
380+
381+
default:
382+
// No recovery possible. Return the same error.
383+
return error;
384+
}
385+
}
386+
361387
#pragma mark - Delete FID
362388

363389
- (FBLPromise<NSNull *> *)deleteInstallation {
@@ -380,9 +406,13 @@ - (FIRInstallationsStoredRegistrationParameters *)currentRegistrationParameters
380406
})
381407
.then(^id(FIRInstallationsItem *installation) {
382408
// Remove the installation from the local storage.
383-
return [self.installationsStore removeInstallationForAppID:installation.appID
384-
appName:installation.firebaseAppName];
385-
})
409+
return [self deleteInstallationLocally:installation];
410+
});
411+
}
412+
413+
- (FBLPromise<NSNull *> *)deleteInstallationLocally:(FIRInstallationsItem *)installation {
414+
return [self.installationsStore removeInstallationForAppID:installation.appID
415+
appName:installation.firebaseAppName]
386416
.then(^FBLPromise<NSNull *> *(NSNull *result) {
387417
return [self deleteExistingIIDIfNeeded];
388418
})

FirebaseInstallations/Source/Tests/Unit/FIRInstallationsAPIServiceTests.m

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ - (void)testRegisterInstallation_WhenError500_ThenRetriesOnce {
178178
NSData *successResponseData =
179179
[self loadFixtureNamed:@"APIRegisterInstallationResponseSuccess.json"];
180180
taskCompletion(successResponseData,
181-
[self responseWithStatusCode:kFIRInstallationsAPIInternalErrorHTTPCode], nil);
181+
[self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError], nil);
182182

183183
// 6.1. Expect network request to send again.
184184
id mockDataTask2 = OCMClassMock([NSURLSessionDataTask class]);
@@ -192,7 +192,7 @@ - (void)testRegisterInstallation_WhenError500_ThenRetriesOnce {
192192

193193
// 6.3. Send network response again.
194194
taskCompletion(successResponseData,
195-
[self responseWithStatusCode:kFIRInstallationsAPIInternalErrorHTTPCode], nil);
195+
[self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError], nil);
196196

197197
// 7. Check result.
198198
XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
@@ -202,7 +202,7 @@ - (void)testRegisterInstallation_WhenError500_ThenRetriesOnce {
202202

203203
XCTAssertTrue([promise.error isKindOfClass:[FIRInstallationsHTTPError class]]);
204204
FIRInstallationsHTTPError *HTTPError = (FIRInstallationsHTTPError *)promise.error;
205-
XCTAssertEqual(HTTPError.HTTPResponse.statusCode, kFIRInstallationsAPIInternalErrorHTTPCode);
205+
XCTAssertEqual(HTTPError.HTTPResponse.statusCode, FIRInstallationsHTTPCodesServerInternalError);
206206
}
207207

208208
- (void)testRefreshAuthTokenSuccess {
@@ -353,7 +353,7 @@ - (void)testRefreshAuthToken_WhenAPIError500_ThenRetriesOnce {
353353

354354
// 5. Call the data task completion.
355355
taskCompletion(errorResponseData,
356-
[self responseWithStatusCode:kFIRInstallationsAPIInternalErrorHTTPCode], nil);
356+
[self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError], nil);
357357

358358
// 6. Retry:
359359

@@ -368,13 +368,14 @@ - (void)testRefreshAuthToken_WhenAPIError500_ThenRetriesOnce {
368368

369369
// 6.2. Send the API response again.
370370
taskCompletion(errorResponseData,
371-
[self responseWithStatusCode:kFIRInstallationsAPIInternalErrorHTTPCode], nil);
371+
[self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError], nil);
372372

373373
// 6. Check result.
374374
XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
375375

376-
XCTAssertTrue([FIRInstallationsErrorUtil isAPIError:promise.error
377-
withHTTPCode:kFIRInstallationsAPIInternalErrorHTTPCode]);
376+
XCTAssertTrue([FIRInstallationsErrorUtil
377+
isAPIError:promise.error
378+
withHTTPCode:FIRInstallationsHTTPCodesServerInternalError]);
378379
XCTAssertNil(promise.value);
379380
}
380381

@@ -549,7 +550,8 @@ - (void)testDeleteInstallation_WhenAPIError500_ThenRetriesOnce {
549550

550551
// 5. Call the data task completion.
551552
// HTTP 200 but no data (a potential server failure).
552-
taskCompletion(nil, [self responseWithStatusCode:kFIRInstallationsAPIInternalErrorHTTPCode], nil);
553+
taskCompletion(nil, [self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError],
554+
nil);
553555

554556
// 6. Retry:
555557
// 6.1. Wait for the API request to be sent again.
@@ -562,13 +564,15 @@ - (void)testDeleteInstallation_WhenAPIError500_ThenRetriesOnce {
562564
OCMVerifyAllWithDelay(mockDataTask1, 1.5);
563565

564566
// 6.1. Send another response.
565-
taskCompletion(nil, [self responseWithStatusCode:kFIRInstallationsAPIInternalErrorHTTPCode], nil);
567+
taskCompletion(nil, [self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError],
568+
nil);
566569

567570
// 7. Check result.
568571
FBLWaitForPromisesWithTimeout(0.5);
569572

570-
XCTAssertTrue([FIRInstallationsErrorUtil isAPIError:promise.error
571-
withHTTPCode:kFIRInstallationsAPIInternalErrorHTTPCode]);
573+
XCTAssertTrue([FIRInstallationsErrorUtil
574+
isAPIError:promise.error
575+
withHTTPCode:FIRInstallationsHTTPCodesServerInternalError]);
572576
XCTAssertNil(promise.value);
573577
}
574578

FirebaseInstallations/Source/Tests/Unit/FIRInstallationsIDControllerTests.m

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,89 @@ - (void)testGetAuthTokenForceRefresh_WhenCalledSeveralTimes_OnlyOneOperationIsPe
553553
}
554554
}
555555

556+
- (void)testGetAuthToken_WhenAPIResponse401_ThenFISResetAndReregistered {
557+
NSTimeInterval timeout = 0.5;
558+
559+
// 1.1. Expect installation to be requested from the store.
560+
FIRInstallationsItem *storedInstallation =
561+
[FIRInstallationsItem createRegisteredInstallationItem];
562+
[self expectInstallationStoreToBeRequestedAndReturnInstallation:storedInstallation];
563+
564+
// 1.2. Expect API request.
565+
FBLPromise *rejectedAPIPromise = [FBLPromise pendingPromise];
566+
OCMExpect([self.mockAPIService refreshAuthTokenForInstallation:storedInstallation])
567+
.andReturn(rejectedAPIPromise);
568+
569+
// 2. Request auth token.
570+
FBLPromise<FIRInstallationsItem *> *promise = [self.controller getAuthTokenForcingRefresh:YES];
571+
572+
// 3. Wait for refresh token request.
573+
OCMVerifyAllWithDelay(self.mockAPIService, timeout);
574+
575+
// 4.1. Expect Installation to be requested before deletion.
576+
[self expectInstallationStoreToBeRequestedAndReturnInstallation:storedInstallation];
577+
// 4. Expect for FIS to be deleted locally.
578+
NSArray<XCTestExpectation *> *deleteExpectations =
579+
[self expectInstallationToBeDeletedLocally:storedInstallation];
580+
581+
// 6. Expect a new installation to be created and registered.
582+
// 6.1. Expect to request FIS from storage.
583+
[self expectInstallationsStoreGetInstallationNotFound];
584+
// 6.2. Expect stored IID not found.
585+
[self expectStoredIIDNotFound];
586+
// 6.3. Expect new Installation to be stored.
587+
__block FIRInstallationsItem *createdInstallation;
588+
OCMExpect([self.mockInstallationsStore
589+
saveInstallation:[OCMArg checkWithBlock:^BOOL(FIRInstallationsItem *obj) {
590+
[self assertValidCreatedInstallation:obj];
591+
592+
createdInstallation = obj;
593+
return YES;
594+
}]])
595+
.andReturn([FBLPromise resolvedWith:[NSNull null]]);
596+
// 6.4. Expect registration API request to be sent.
597+
FBLPromise<FIRInstallationsItem *> *registerPromise = [FBLPromise pendingPromise];
598+
OCMExpect([self.mockAPIService registerInstallation:[OCMArg any]]).andReturn(registerPromise);
599+
600+
// 6.5. Reject API request promise with 401.
601+
NSError *error401 = [FIRInstallationsErrorUtil APIErrorWithHTTPCode:404];
602+
[rejectedAPIPromise reject:error401];
603+
// 6.6. Wait local FIS to be deleted.
604+
[self waitForExpectations:deleteExpectations timeout:timeout];
605+
606+
// 6.7 Wait for the new Installation to be stored.
607+
OCMVerifyAllWithDelay(self.mockInstallationsStore, timeout);
608+
// 6.8. Wait for registration API request to be sent.
609+
OCMVerifyAllWithDelay(self.mockAPIService, timeout);
610+
// 6.9. Expect for the registered installation to be saved.
611+
FIRInstallationsItem *registeredInstallation = [FIRInstallationsItem
612+
createRegisteredInstallationItemWithAppID:createdInstallation.appID
613+
appName:createdInstallation.firebaseAppName];
614+
615+
OCMExpect([self.mockInstallationsStore
616+
saveInstallation:[OCMArg checkWithBlock:^BOOL(FIRInstallationsItem *obj) {
617+
XCTAssertEqual(registeredInstallation, obj);
618+
return YES;
619+
}]])
620+
.andReturn([FBLPromise resolvedWith:[NSNull null]]);
621+
// 6.9. Fulfill the registration API request promise.
622+
[registerPromise fulfill:registeredInstallation];
623+
624+
// 7. Wait for promises.
625+
XCTAssert(FBLWaitForPromisesWithTimeout(timeout));
626+
627+
// 8. Check.
628+
OCMVerifyAll(self.mockInstallationsStore);
629+
OCMVerifyAll(self.mockAPIService);
630+
631+
XCTAssertNil(promise.error);
632+
XCTAssertNotNil(promise.value);
633+
634+
XCTAssertNotEqualObjects(promise.value.firebaseInstallationID,
635+
storedInstallation.firebaseInstallationID);
636+
XCTAssertEqualObjects(promise.value, registeredInstallation);
637+
}
638+
556639
#pragma mark - FID Deletion
557640

558641
- (void)testDeleteRegisteredInstallation {
@@ -642,7 +725,7 @@ - (void)testDeleteRegisteredInstallation_WhenAPIRequestFails_ThenFailsAndInstall
642725
// 2. Expect API request to delete installation.
643726
FBLPromise *rejectedAPIPromise = [FBLPromise pendingPromise];
644727
NSError *error500 =
645-
[FIRInstallationsErrorUtil APIErrorWithHTTPCode:kFIRInstallationsAPIInternalErrorHTTPCode];
728+
[FIRInstallationsErrorUtil APIErrorWithHTTPCode:FIRInstallationsHTTPCodesServerInternalError];
646729
[rejectedAPIPromise reject:error500];
647730
OCMExpect([self.mockAPIService deleteInstallation:installation]).andReturn(rejectedAPIPromise);
648731

@@ -814,6 +897,24 @@ - (void)testDeleteInstallation_WhenNotDefaultApp_ThenIIDIsNotDeleted {
814897
OCMVerifyAll(self.mockIIDStore);
815898
}
816899

900+
- (NSArray<XCTestExpectation *> *)expectInstallationToBeDeletedLocally:
901+
(FIRInstallationsItem *)installation {
902+
// 3.1. Expect the installation to be removed from the storage.
903+
OCMExpect([self.mockInstallationsStore removeInstallationForAppID:installation.appID
904+
appName:installation.firebaseAppName])
905+
.andReturn([FBLPromise resolvedWith:[NSNull null]]);
906+
907+
// 3.2. Expect IID to be deleted, because it is default app.
908+
OCMExpect([self.mockIIDStore deleteExistingIID])
909+
.andReturn([FBLPromise resolvedWith:[NSNull null]]);
910+
911+
// 4. Expect FIRInstallationIDDidChangeNotification to be sent.
912+
XCTestExpectation *notificationExpectation =
913+
[self installationIDDidChangeNotificationExpectation];
914+
915+
return @[ notificationExpectation ];
916+
}
917+
817918
// TODO: Test a single delete installation request at a time.
818919

819920
#pragma mark - Notifications
@@ -1087,6 +1188,12 @@ - (void)expectInstallationsStoreGetInstallationNotFound {
10871188
.andReturn(installationNotFoundPromise);
10881189
}
10891190

1191+
- (void)expectStoredIIDNotFound {
1192+
FBLPromise *rejectedPromise = [FBLPromise pendingPromise];
1193+
[rejectedPromise reject:[FIRInstallationsErrorUtil keychainErrorWithFunction:@"" status:-1]];
1194+
OCMExpect([self.mockIIDStore existingIID]).andReturn(rejectedPromise);
1195+
}
1196+
10901197
- (void)assertValidCreatedInstallation:(FIRInstallationsItem *)installation {
10911198
XCTAssertEqualObjects([installation class], [FIRInstallationsItem class]);
10921199
XCTAssertEqualObjects(installation.appID, self.appID);
@@ -1138,4 +1245,10 @@ - (FIRInstallationsStoredRegistrationParameters *)otherRegistrationParameters {
11381245
projectID:projectID];
11391246
}
11401247

1248+
- (void)expectInstallationStoreToBeRequestedAndReturnInstallation:
1249+
(FIRInstallationsItem *)storedInstallation {
1250+
OCMExpect([self.mockInstallationsStore installationForAppID:self.appID appName:self.appName])
1251+
.andReturn([FBLPromise resolvedWith:storedInstallation]);
1252+
}
1253+
11411254
@end

FirebaseInstallations/Source/Tests/Unit/FIRInstallationsTests.m

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ - (void)testAuthTokenSuccess {
137137
- (void)testAuthTokenError {
138138
FBLPromise *errorPromise = [FBLPromise pendingPromise];
139139
[errorPromise reject:[FIRInstallationsErrorUtil
140-
APIErrorWithHTTPCode:kFIRInstallationsAPIInternalErrorHTTPCode]];
140+
APIErrorWithHTTPCode:FIRInstallationsHTTPCodesServerInternalError]];
141141
OCMExpect([self.mockIDController getAuthTokenForcingRefresh:NO]).andReturn(errorPromise);
142142

143143
XCTestExpectation *tokenExpectation = [self expectationWithDescription:@"AuthTokenSuccess"];
@@ -186,7 +186,7 @@ - (void)testAuthTokenForcingRefreshSuccess {
186186
- (void)testAuthTokenForcingRefreshError {
187187
FBLPromise *errorPromise = [FBLPromise pendingPromise];
188188
[errorPromise reject:[FIRInstallationsErrorUtil
189-
APIErrorWithHTTPCode:kFIRInstallationsAPIInternalErrorHTTPCode]];
189+
APIErrorWithHTTPCode:FIRInstallationsHTTPCodesServerInternalError]];
190190
OCMExpect([self.mockIDController getAuthTokenForcingRefresh:YES]).andReturn(errorPromise);
191191

192192
XCTestExpectation *tokenExpectation = [self expectationWithDescription:@"AuthTokenSuccess"];
@@ -221,7 +221,7 @@ - (void)testDeleteSuccess {
221221
- (void)testDeleteError {
222222
FBLPromise *errorPromise = [FBLPromise pendingPromise];
223223
NSError *APIError =
224-
[FIRInstallationsErrorUtil APIErrorWithHTTPCode:kFIRInstallationsAPIInternalErrorHTTPCode];
224+
[FIRInstallationsErrorUtil APIErrorWithHTTPCode:FIRInstallationsHTTPCodesServerInternalError];
225225
[errorPromise reject:APIError];
226226
OCMExpect([self.mockIDController deleteInstallation]).andReturn(errorPromise);
227227

0 commit comments

Comments
 (0)