/* * Copyright 2019 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 #import #import #import "Firebase/InstanceID/FIRInstanceIDAuthService.h" #import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h" #import "Firebase/InstanceID/FIRInstanceIDCheckinService.h" #import "Firebase/InstanceID/FIRInstanceIDConstants.h" #import "Firebase/InstanceID/FIRInstanceIDKeychain.h" #import "Firebase/InstanceID/FIRInstanceIDStore.h" #import "Firebase/InstanceID/FIRInstanceIDTokenDeleteOperation.h" #import "Firebase/InstanceID/FIRInstanceIDTokenFetchOperation.h" #import "Firebase/InstanceID/FIRInstanceIDTokenOperation+Private.h" #import "Firebase/InstanceID/FIRInstanceIDTokenOperation.h" #import "Firebase/InstanceID/NSError+FIRInstanceID.h" #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h" #import "GoogleUtilities/Environment/Private/GULHeartbeatDateStorage.h" static NSString *kDeviceID = @"fakeDeviceID"; static NSString *kSecretToken = @"fakeSecretToken"; static NSString *kDigestString = @"test-digest"; static NSString *kVersionInfoString = @"version_info-1.0.0"; static NSString *kAuthorizedEntity = @"sender-1234567"; static NSString *kScope = @"fcm"; static NSString *kRegistrationToken = @"token-12345"; @interface FIRInstanceIDTokenOperation (ExposedForTest) - (void)performTokenOperation; @end @interface FIRInstallationsAuthTokenResult (Tests) - (instancetype)initWithToken:(NSString *)token expirationDate:(NSDate *)expirationDate; @end @interface FIRInstanceIDTokenOperationsTest : XCTestCase @property(strong, readonly, nonatomic) FIRInstanceIDAuthService *authService; @property(strong, readonly, nonatomic) id mockAuthService; @property(strong, readonly, nonatomic) id mockStore; @property(strong, readonly, nonatomic) FIRInstanceIDCheckinService *checkinService; @property(strong, readonly, nonatomic) id mockCheckinService; @property(strong, readonly, nonatomic) id mockInstallations; @property(strong, readonly, nonatomic) NSString *instanceID; @property(nonatomic, readwrite, strong) FIRInstanceIDCheckinPreferences *checkinPreferences; @end @implementation FIRInstanceIDTokenOperationsTest - (void)setUp { [super setUp]; _mockStore = OCMClassMock([FIRInstanceIDStore class]); _checkinService = [[FIRInstanceIDCheckinService alloc] init]; _mockCheckinService = OCMPartialMock(_checkinService); _authService = [[FIRInstanceIDAuthService alloc] initWithCheckinService:_mockCheckinService store:_mockStore]; _instanceID = @"instanceID"; // `FIRInstanceIDTokenOperation` uses `FIRInstallations` under the hood to get FIS auth token. // Stub `FIRInstallations` to avoid using a real object. [self stubInstallations]; NSString *const kHeartbeatStorageFile = @"HEARTBEAT_INFO_STORAGE"; GULHeartbeatDateStorage *dataStorage = [[GULHeartbeatDateStorage alloc] initWithFileName:kHeartbeatStorageFile]; [[NSFileManager defaultManager] removeItemAtURL:[dataStorage fileURL] error:nil]; } - (void)tearDown { [_mockInstallations stopMocking]; _mockInstallations = nil; _authService = nil; [_mockCheckinService stopMocking]; _mockCheckinService = nil; _checkinService = nil; _mockStore = nil; } - (void)testThatTokenOperationsAuthHeaderStringMatchesCheckin { int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000; FIRInstanceIDCheckinPreferences *checkin = [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo]; NSString *expectedAuthHeader = [FIRInstanceIDTokenOperation HTTPAuthHeaderFromCheckin:checkin]; XCTestExpectation *authHeaderMatchesCheckinExpectation = [self expectationWithDescription:@"Auth header string in request matches checkin info"]; FIRInstanceIDTokenFetchOperation *operation = [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity scope:kScope options:nil checkinPreferences:checkin instanceID:self.instanceID]; operation.testBlock = ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) { NSDictionary *headers = request.allHTTPHeaderFields; NSString *authHeader = headers[@"Authorization"]; if ([authHeader isEqualToString:expectedAuthHeader]) { [authHeaderMatchesCheckinExpectation fulfill]; } // Return a response (doesnt matter what the response is) NSData *responseBody = [self dataForFetchRequest:request returnValidToken:YES]; NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:nil]; response(responseBody, responseObject, nil); }; [operation start]; [self waitForExpectationsWithTimeout:0.25 handler:^(NSError *_Nullable error) { XCTAssertNil(error.localizedDescription); }]; } - (void)testThatTokenOperationWithoutCheckInFails { // If asserts are enabled, test for the assert to be thrown, otherwise check for the resulting // error in the completion handler. XCTestExpectation *failedExpectation = [self expectationWithDescription:@"Operation failed without checkin info"]; // This will return hasCheckinInfo == NO FIRInstanceIDCheckinPreferences *emptyCheckinPreferences = [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:@"" secretToken:@""]; FIRInstanceIDTokenOperation *operation = [[FIRInstanceIDTokenOperation alloc] initWithAction:FIRInstanceIDTokenActionFetch forAuthorizedEntity:kAuthorizedEntity scope:kScope options:nil checkinPreferences:emptyCheckinPreferences instanceID:self.instanceID]; [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result, NSString *_Nullable token, NSError *_Nullable error) { [failedExpectation fulfill]; }]; @try { [operation start]; } @catch (NSException *exception) { if (exception.name == NSInternalInconsistencyException) { [failedExpectation fulfill]; } } @finally { } [self waitForExpectationsWithTimeout:0.25 handler:^(NSError *_Nullable error) { XCTAssertNil(error.localizedDescription); }]; } - (void)testThatAnAlreadyCancelledOperationFinishesWithoutStarting { XCTestExpectation *cancelledExpectation = [self expectationWithDescription:@"Operation finished as cancelled"]; XCTestExpectation *didNotCallPerform = [self expectationWithDescription:@"Did not call performTokenOperation"]; __block BOOL performWasCalled = NO; int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000; FIRInstanceIDCheckinPreferences *checkinPreferences = [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo]; FIRInstanceIDTokenOperation *operation = [[FIRInstanceIDTokenOperation alloc] initWithAction:FIRInstanceIDTokenActionFetch forAuthorizedEntity:kAuthorizedEntity scope:kScope options:nil checkinPreferences:checkinPreferences instanceID:self.instanceID]; [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result, NSString *_Nullable token, NSError *_Nullable error) { if (result == FIRInstanceIDTokenOperationCancelled) { [cancelledExpectation fulfill]; } if (!performWasCalled) { [didNotCallPerform fulfill]; } }]; id mockOperation = OCMPartialMock(operation); [[[mockOperation stub] andDo:^(NSInvocation *invocation) { performWasCalled = YES; }] performTokenOperation]; [operation cancel]; [operation start]; [self waitForExpectationsWithTimeout:0.25 handler:^(NSError *_Nullable error) { XCTAssertNil(error.localizedDescription); }]; } - (void)testThatOptionsDictionaryIsIncludedWithFetchRequest { XCTestExpectation *optionsIncludedExpectation = [self expectationWithDescription:@"Options keys were included in token URL request"]; int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000; FIRInstanceIDCheckinPreferences *checkinPreferences = [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo]; NSData *fakeDeviceToken = [@"fakeAPNSToken" dataUsingEncoding:NSUTF8StringEncoding]; BOOL isSandbox = NO; NSString *apnsTupleString = FIRInstanceIDAPNSTupleStringForTokenAndServerType(fakeDeviceToken, isSandbox); NSDictionary *options = @{ kFIRInstanceIDTokenOptionsFirebaseAppIDKey : @"fakeGMPAppID", kFIRInstanceIDTokenOptionsAPNSKey : fakeDeviceToken, kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(isSandbox), }; FIRInstanceIDTokenFetchOperation *operation = [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity scope:kScope options:options checkinPreferences:checkinPreferences instanceID:self.instanceID]; operation.testBlock = ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) { NSString *query = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding]; NSString *gmpAppIDQueryTuple = [NSString stringWithFormat:@"%@=%@", kFIRInstanceIDTokenOptionsFirebaseAppIDKey, options[kFIRInstanceIDTokenOptionsFirebaseAppIDKey]]; NSRange gmpAppIDRange = [query rangeOfString:gmpAppIDQueryTuple]; NSString *apnsQueryTuple = [NSString stringWithFormat:@"%@=%@", kFIRInstanceIDTokenOptionsAPNSKey, apnsTupleString]; NSRange apnsRange = [query rangeOfString:apnsQueryTuple]; if (gmpAppIDRange.location != NSNotFound && apnsRange.location != NSNotFound) { [optionsIncludedExpectation fulfill]; } // Return a response (doesnt matter what the response is) NSData *responseBody = [self dataForFetchRequest:request returnValidToken:YES]; NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:nil]; response(responseBody, responseObject, nil); }; [operation start]; [self waitForExpectationsWithTimeout:0.25 handler:^(NSError *_Nullable error) { XCTAssertNil(error.localizedDescription); }]; } - (void)testServerResetCommand { XCTestExpectation *shouldResetIdentityExpectation = [self expectationWithDescription: @"When server sends down RST error, clients should return reset identity error."]; int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000; FIRInstanceIDCheckinPreferences *checkinPreferences = [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo]; FIRInstanceIDTokenFetchOperation *operation = [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity scope:kScope options:nil checkinPreferences:checkinPreferences instanceID:self.instanceID]; operation.testBlock = ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) { // Return a response with Error=RST NSData *responseBody = [self dataForFetchRequest:request returnValidToken:NO]; NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:nil]; response(responseBody, responseObject, nil); }; [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result, NSString *_Nullable token, NSError *_Nullable error) { XCTAssertEqual(result, FIRInstanceIDTokenOperationError); XCTAssertNotNil(error); XCTAssertEqual(error.code, kFIRInstanceIDErrorCodeInvalidIdentity); [shouldResetIdentityExpectation fulfill]; }]; [operation start]; [self waitForExpectationsWithTimeout:0.25 handler:^(NSError *_Nullable error) { XCTAssertNil(error.localizedDescription); }]; } - (void)testHTTPAuthHeaderGenerationFromCheckin { FIRInstanceIDCheckinPreferences *checkinPreferences = [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceID secretToken:kSecretToken]; NSString *expectedHeader = [NSString stringWithFormat:@"AidLogin %@:%@", checkinPreferences.deviceID, checkinPreferences.secretToken]; NSString *generatedHeader = [FIRInstanceIDTokenOperation HTTPAuthHeaderFromCheckin:checkinPreferences]; XCTAssertEqualObjects(generatedHeader, expectedHeader); } - (void)testTokenFetchOperationFirebaseUserAgentAndHeartbeatHeader { XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completionExpectation"]; FIRInstanceIDCheckinPreferences *checkinPreferences = [self setCheckinPreferencesWithLastCheckinTime:0]; FIRInstanceIDTokenFetchOperation *operation = [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity scope:kScope options:nil checkinPreferences:checkinPreferences instanceID:self.instanceID]; operation.testBlock = ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) { NSString *userAgentValue = request.allHTTPHeaderFields[kFIRInstanceIDFirebaseUserAgentKey]; XCTAssertEqualObjects(userAgentValue, [FIRApp firebaseUserAgent]); NSString *heartBeatCode = request.allHTTPHeaderFields[kFIRInstanceIDFirebaseHeartbeatKey]; XCTAssertEqualObjects(heartBeatCode, @"3"); // Return a response with Error=RST NSData *responseBody = [self dataForFetchRequest:request returnValidToken:NO]; NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:nil]; response(responseBody, responseObject, nil); }; [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result, NSString *_Nullable token, NSError *_Nullable error) { [completionExpectation fulfill]; }]; [operation start]; [self waitForExpectationsWithTimeout:0.25 handler:^(NSError *_Nullable error) { XCTAssertNil(error.localizedDescription); }]; } #pragma mark - Internal Helpers - (NSData *)dataForFetchRequest:(NSURLRequest *)request returnValidToken:(BOOL)returnValidToken { NSString *response; if (returnValidToken) { response = [NSString stringWithFormat:@"token=%@", kRegistrationToken]; } else { response = @"Error=RST"; } return [response dataUsingEncoding:NSUTF8StringEncoding]; } - (FIRInstanceIDCheckinPreferences *)setCheckinPreferencesWithLastCheckinTime:(int64_t)time { FIRInstanceIDCheckinPreferences *checkinPreferences = [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceID secretToken:kSecretToken]; NSDictionary *checkinPlistContents = @{ kFIRInstanceIDDigestStringKey : kDigestString, kFIRInstanceIDVersionInfoStringKey : kVersionInfoString, kFIRInstanceIDLastCheckinTimeKey : @(time) }; [checkinPreferences updateWithCheckinPlistContents:checkinPlistContents]; // manually initialize the checkin preferences self.checkinPreferences = checkinPreferences; return checkinPreferences; } - (void)stubInstallations { _mockInstallations = OCMClassMock([FIRInstallations class]); OCMStub([_mockInstallations installations]).andReturn(_mockInstallations); FIRInstallationsAuthTokenResult *authToken = [[FIRInstallationsAuthTokenResult alloc] initWithToken:@"fis-auth-token" expirationDate:[NSDate distantFuture]]; id authTokenWithCompletionArg = [OCMArg invokeBlockWithArgs:authToken, [NSNull null], nil]; OCMStub([_mockInstallations authTokenWithCompletion:authTokenWithCompletionArg]); } @end