Files
firebase-ios-sdk/Example/InstanceID/Tests/FIRInstanceIDAuthServiceTest.m
T

401 lines
17 KiB
Objective-C

/*
* 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 <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import "Firebase/InstanceID/FIRInstanceIDAuthService.h"
#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h"
#import "Firebase/InstanceID/FIRInstanceIDCheckinService.h"
#import "Firebase/InstanceID/FIRInstanceIDStore.h"
#import "Firebase/InstanceID/NSError+FIRInstanceID.h"
static NSString *const kDeviceAuthId = @"device-id";
static NSString *const kSecretToken = @"secret-token";
static NSString *const kVersionInfo = @"1.0";
@interface FIRInstanceIDCheckinService ()
@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinPreferences *checkinPreferences;
@end
@interface FIRInstanceIDAuthService ()
@property(atomic, readwrite, assign) int64_t lastCheckinTimestampSeconds;
@property(atomic, readwrite, assign) int64_t nextScheduledCheckinIntervalSeconds;
@property(atomic, readwrite, assign) int checkinRetryCount;
@property(nonatomic, readonly, strong)
NSMutableArray<FIRInstanceIDDeviceCheckinCompletion> *checkinHandlers;
@end
@interface FIRInstanceIDAuthServiceTest : XCTestCase
@property(nonatomic, readwrite, strong) FIRInstanceIDAuthService *authService;
@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinService *checkinService;
@property(nonatomic, readwrite, strong) id mockCheckinService;
@property(nonatomic, readwrite, strong) id mockStore;
@property(nonatomic, readwrite, copy) FIRInstanceIDDeviceCheckinCompletion checkinCompletion;
@end
@implementation FIRInstanceIDAuthServiceTest
- (void)setUp {
[super setUp];
_mockStore = OCMClassMock([FIRInstanceIDStore class]);
_checkinService = [[FIRInstanceIDCheckinService alloc] init];
_mockCheckinService = OCMPartialMock(_checkinService);
_authService = [[FIRInstanceIDAuthService alloc] initWithCheckinService:_mockCheckinService
store:_mockStore];
// The tests here are to focus on checkin interval not locale change, so always set locale as
// non-changed.
[[NSUserDefaults standardUserDefaults] setObject:FIRInstanceIDCurrentLocale()
forKey:kFIRInstanceIDUserDefaultsKeyLocale];
}
- (void)tearDown {
_checkinCompletion = nil;
[super tearDown];
}
/**
* Test scheduling a checkin which completes successfully. Once the checkin is complete
* we should have the valid checkin preferences in memory.
*/
- (void)testScheduleCheckin_initialSuccess {
XCTestExpectation *checkinExpectation =
[self expectationWithDescription:@"Did call checkin service"];
FIRInstanceIDCheckinPreferences *checkinPreferences = [self validCheckinPreferences];
[[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
self.checkinCompletion(checkinPreferences, nil);
}] checkinWithExistingCheckin:[OCMArg any]
completion:[OCMArg checkWithBlock:^BOOL(id obj) {
[checkinExpectation fulfill];
self.checkinCompletion = obj;
return obj != nil;
}]];
// Always return YES for whether we succeeded in persisting the checkin
[[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
[self.authService scheduleCheckin:YES];
XCTAssertTrue([self.authService hasValidCheckinInfo]);
XCTAssertEqual([self.authService checkinRetryCount], 1);
[self waitForExpectationsWithTimeout:2.0 handler:NULL];
}
/**
* Test scheduling a checkin which completes successfully, but fails to save, due to Keychain
* errors.
*/
- (void)testScheduleCheckin_successButFailureInSaving {
XCTestExpectation *checkinFailureExpectation =
[self expectationWithDescription:@"Did receive error after checkin"];
FIRInstanceIDCheckinPreferences *checkinPreferences = [self validCheckinPreferences];
[[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
self.checkinCompletion(checkinPreferences, nil);
}] checkinWithExistingCheckin:[OCMArg any]
completion:[OCMArg checkWithBlock:^BOOL(id obj) {
self.checkinCompletion = obj;
return obj != nil;
}]];
// Always return NO for whether we succeeded in persisting the checkin, to simulate Keychain error
[[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
handler:[OCMArg invokeBlockWithArgs:[OCMArg any], nil]];
[self.authService
fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *checkin, NSError *error) {
[checkinFailureExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:2.0 handler:NULL];
XCTAssertFalse([self.authService hasValidCheckinInfo]);
}
/**
* Test scheduling multiple checkins to complete immediately. Each successive checkin should
* be triggered immediately.
*/
- (void)testMultipleScheduleCheckin_immediately {
XCTestExpectation *checkinExpectation =
[self expectationWithDescription:@"Did call checkin service"];
__block int checkinHandlerInvocationCount = 0;
FIRInstanceIDCheckinPreferences *checkinPreferences = [self validCheckinPreferences];
[[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
checkinHandlerInvocationCount++;
// Mock successful Checkin after delay.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[checkinExpectation fulfill];
self.checkinCompletion(checkinPreferences, nil);
});
}] checkinWithExistingCheckin:[OCMArg any]
completion:[OCMArg checkWithBlock:^BOOL(id obj) {
self.checkinCompletion = obj;
return obj != nil;
}]];
// Always return YES for whether we succeeded in persisting the checkin
[[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
[self.authService scheduleCheckin:YES];
// Schedule an immediate checkin again.
// This should just return because the previous checkin isn't over yet.
[self.authService scheduleCheckin:YES];
[self waitForExpectationsWithTimeout:5.0 handler:NULL];
XCTAssertTrue([self.authService hasValidCheckinInfo]);
XCTAssertEqual([self.authService checkinRetryCount], 2);
// Checkin handler should only be invoked once since the second checkin request should
// return immediately.
XCTAssertEqual(checkinHandlerInvocationCount, 1);
}
/**
* Test multiple checkins scheduled. The second checkin should be scheduled after some
* delay before the first checkin has returned. Since the latter checkin is not immediate
* we should not run it since the first checkin is already scheduled to be executed later.
*/
- (void)testMultipleScheduleCheckin_notImmediately {
XCTestExpectation *checkinExpectation =
[self expectationWithDescription:@"Did call checkin service"];
FIRInstanceIDCheckinPreferences *checkinPreferences = [self validCheckinPreferences];
[[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
// Mock successful Checkin after delay.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[checkinExpectation fulfill];
self.checkinCompletion(checkinPreferences, nil);
});
}] checkinWithExistingCheckin:[OCMArg any]
completion:[OCMArg checkWithBlock:^BOOL(id obj) {
self.checkinCompletion = obj;
return obj != nil;
}]];
// Always return YES for whether we succeeded in persisting the checkin
[[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
[self.authService scheduleCheckin:YES];
// Schedule another checkin after some delay while the first checkin has not yet returned
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[self.authService scheduleCheckin:NO];
});
[self waitForExpectationsWithTimeout:5.0 handler:NULL];
XCTAssertTrue([self.authService hasValidCheckinInfo]);
XCTAssertEqual([self.authService checkinRetryCount], 1);
}
/**
* Test initial checkin failure which schedules another checkin which should succeed.
*/
- (void)testInitialCheckinFailure_retrySuccess {
XCTestExpectation *checkinExpectation =
[self expectationWithDescription:@"Did call checkin service"];
__block int checkinHandlerInvocationCount = 0;
[[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
checkinHandlerInvocationCount++;
if (checkinHandlerInvocationCount == 1) {
// Mock failure on first try
NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeTimeout];
self.checkinCompletion(nil, error);
} else if (checkinHandlerInvocationCount == 2) {
// Mock success on second try
[checkinExpectation fulfill];
self.checkinCompletion([self validCheckinPreferences], nil);
} else {
// We should not retry for a third time again.
XCTFail(@"Invoking checkin handler invalid number of times.");
}
}] checkinWithExistingCheckin:[OCMArg any]
completion:[OCMArg checkWithBlock:^BOOL(id obj) {
self.checkinCompletion = obj;
return obj != nil;
}]];
// Always return YES for whether we succeeded in persisting the checkin
[[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
[self.authService scheduleCheckin:YES];
// Schedule another checkin after some delay while the first checkin has not yet returned
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[self.authService scheduleCheckin:YES];
XCTAssertTrue([self.authService hasValidCheckinInfo]);
XCTAssertEqual([self.authService checkinRetryCount], 2);
XCTAssertEqual(checkinHandlerInvocationCount, 2);
});
[self waitForExpectationsWithTimeout:5.0 handler:NULL];
}
/**
* Test initial checkin failure which schedules another checkin which should succeed. If
* a new checkin request comes after that we should not schedule a checkin as we have
* already have valid checkin credentials.
*/
- (void)testInitialCheckinFailure_multipleRetrySuccess {
XCTestExpectation *checkinExpectation =
[self expectationWithDescription:@"Did call checkin service"];
__block int checkinHandlerInvocationCount = 0;
[[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
checkinHandlerInvocationCount++;
if (checkinHandlerInvocationCount <= 2) {
// Mock failure on first try
NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeTimeout];
self.checkinCompletion(nil, error);
} else if (checkinHandlerInvocationCount == 3) {
// Mock success on second try
[checkinExpectation fulfill];
self.checkinCompletion([self validCheckinPreferences], nil);
} else {
// We should not retry for a third time again.
XCTFail(@"Invoking checkin handler invalid number of times.");
}
}] checkinWithExistingCheckin:[OCMArg any]
completion:[OCMArg checkWithBlock:^BOOL(id obj) {
self.checkinCompletion = obj;
return obj != nil;
}]];
// Always return YES for whether we succeeded in persisting the checkin
[[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
[self.authService scheduleCheckin:YES];
[self waitForExpectationsWithTimeout:10.0 handler:NULL];
XCTAssertTrue([self.authService hasValidCheckinInfo]);
XCTAssertEqual([self.authService checkinRetryCount], 3);
}
/**
* Performing multiple checkin requests should result in multiple handlers being
* called back, but with only a single actual checkin fetch.
*/
- (void)testMultipleCheckinHandlersWithSuccessfulCheckin {
XCTestExpectation *allHandlersCalledExpectation =
[self expectationWithDescription:@"All checkin handlers were called"];
__block NSInteger checkinHandlerCallbackCount = 0;
__block NSInteger checkinServiceInvocationCount = 0;
// Always return a successful checkin, and count the number of times CheckinService is called
[[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
checkinServiceInvocationCount++;
self.checkinCompletion([self validCheckinPreferences], nil);
}] checkinWithExistingCheckin:[OCMArg any]
completion:[OCMArg checkWithBlock:^BOOL(id obj) {
self.checkinCompletion = obj;
return obj != nil;
}]];
// Always return YES for whether we succeeded in persisting the checkin
[[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
NSInteger numHandlers = 10;
for (NSInteger i = 0; i < numHandlers; i++) {
[self.authService
fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *checkin, NSError *error) {
checkinHandlerCallbackCount++;
if (checkinHandlerCallbackCount == numHandlers) {
[allHandlersCalledExpectation fulfill];
}
}];
}
[self waitForExpectationsWithTimeout:1.0 handler:nil];
XCTAssertEqual(checkinServiceInvocationCount, 1);
XCTAssertEqual(checkinHandlerCallbackCount, numHandlers);
}
/**
* Performing a scheduled checkin *and* simultaneous checkin request should result in
* the number of pending checkin handlers to be 2 (one for the scheduled checkin, one for
* the direct fetch).
*/
- (void)testScheduledAndImmediateCheckinsWithMultipleHandler {
XCTestExpectation *fetchHandlerCalledExpectation =
[self expectationWithDescription:@"Direct checkin handler was called"];
__block NSInteger checkinServiceInvocationCount = 0;
// Always return a successful checkin, and count the number of times CheckinService is called
[[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
checkinServiceInvocationCount++;
// Give the checkin service some time to complete the request
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
self.checkinCompletion([self validCheckinPreferences], nil);
});
}] checkinWithExistingCheckin:[OCMArg any]
completion:[OCMArg checkWithBlock:^BOOL(id obj) {
self.checkinCompletion = obj;
return obj != nil;
}]];
// Always return YES for whether we succeeded in persisting the checkin
[[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
// Start a scheduled (though immediate) checkin
[self.authService scheduleCheckin:YES];
// Request a direct checkin fetch
[self.authService
fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *checkin, NSError *error) {
[fetchHandlerCalledExpectation fulfill];
}];
// At this point we should have checkinHandlers, one for scheduled, one for the direct fetch
XCTAssertEqual(self.authService.checkinHandlers.count, 2);
[self waitForExpectationsWithTimeout:0.5 handler:nil];
// Make sure only one checkin fetch was performed
XCTAssertEqual(checkinServiceInvocationCount, 1);
}
#pragma mark - Helper Methods
- (FIRInstanceIDCheckinPreferences *)validCheckinPreferences {
NSDictionary *gservicesData = @{
kFIRInstanceIDVersionInfoStringKey : kVersionInfo,
kFIRInstanceIDLastCheckinTimeKey : @(FIRInstanceIDCurrentTimestampInMilliseconds())
};
FIRInstanceIDCheckinPreferences *checkinPreferences =
[[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceAuthId
secretToken:kSecretToken];
[checkinPreferences updateWithCheckinPlistContents:gservicesData];
return checkinPreferences;
}
@end