diff --git a/FirebaseAppDistribution.podspec b/FirebaseAppDistribution.podspec index e890b77770..16d37b10f2 100644 --- a/FirebaseAppDistribution.podspec +++ b/FirebaseAppDistribution.podspec @@ -36,8 +36,9 @@ iOS SDK for App Distribution for Firebase. } s.test_spec 'unit' do |unit_tests| - unit_tests.source_files = 'FirebaseAppDistribution/Tests/Unit*/*.[mh]' - unit_tests.resources = 'FirebaseAppDistribution/Tests/Unit/Resources/*' + unit_tests.source_files = 'FirebaseAppDistribution/Tests/Unit*/*.[mh]' + unit_tests.resources = 'FirebaseAppDistribution/Tests/Unit/Resources/*' + unit_tests.dependency 'OCMock' end # end diff --git a/FirebaseAppDistribution/Sources/FIRAppDistribution.m b/FirebaseAppDistribution/Sources/FIRAppDistribution.m index 0bd96e5bb0..8ad8c1d34b 100644 --- a/FirebaseAppDistribution/Sources/FIRAppDistribution.m +++ b/FirebaseAppDistribution/Sources/FIRAppDistribution.m @@ -13,6 +13,7 @@ // limitations under the License. #import "FIRAppDistribution+Private.h" +#import "FIRAppDistributionAuthPersistence+Private.h" #import "FIRAppDistributionMachO+Private.h" #import "FIRAppDistributionRelease+Private.h" @@ -60,8 +61,15 @@ NSString *const kAppDistroLibraryName = @"fire-fad"; [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor]; } - // TODO: Lookup keychain to load auth state on init + NSError *authRetrievalError; + self.authState = [FIRAppDistributionAuthPersistence retrieveAuthState:&authRetrievalError]; + // TODO (schnecle): replace NSLog statement with FIRLogger log statement + if (authRetrievalError) { + NSLog(@"Error retrieving token from keychain: %@", [authRetrievalError localizedDescription]); + } + self.isTesterSignedIn = self.authState ? YES : NO; + return self; } @@ -128,43 +136,62 @@ NSString *const kAppDistroLibraryName = @"fire-fad"; } - (void)signOutTester { + NSError *error; + BOOL didClearAuthState = [FIRAppDistributionAuthPersistence clearAuthState:&error]; + // TODO (schnecle): Add in FIRLogger to report when we have failed to clear auth state + if (!didClearAuthState) { + NSLog(@"Error clearing token from keychain: %@", [error localizedDescription]); + } + self.authState = nil; self.isTesterSignedIn = false; } - (void)fetchReleases:(FIRAppDistributionUpdateCheckCompletion)completion { - NSURLSession *URLSession = [NSURLSession sharedSession]; - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; - NSString *URLString = - [NSString stringWithFormat:kReleasesEndpointURL, [[FIRApp defaultApp] options].googleAppID]; - [request setURL:[NSURL URLWithString:URLString]]; - [request setHTTPMethod:@"GET"]; - [request setValue:[NSString - stringWithFormat:@"Bearer %@", self.authState.lastTokenResponse.accessToken] - forHTTPHeaderField:@"Authorization"]; + [self.authState performActionWithFreshTokens:^(NSString *_Nonnull accessToken, + NSString *_Nonnull idToken, + NSError *_Nullable error) { + if (error) { + // TODO (schnecle): Add in FIRLogger log statement + NSLog(@"Error fetching fresh tokens: %@", [error localizedDescription]); + [self signOutTester]; + return; + } - NSURLSessionDataTask *listReleasesDataTask = [URLSession - dataTaskWithRequest:request - completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (error) { - // TODO: Reformat error into error code - completion(nil, error); - return; - } + // perform your API request using the tokens + NSURLSession *URLSession = [NSURLSession sharedSession]; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + NSString *URLString = + [NSString stringWithFormat:kReleasesEndpointURL, [[FIRApp defaultApp] options].googleAppID]; + [request setURL:[NSURL URLWithString:URLString]]; + [request setHTTPMethod:@"GET"]; + [request setValue:[NSString stringWithFormat:@"Bearer %@", accessToken] + forHTTPHeaderField:@"Authorization"]; - NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response; + NSURLSessionDataTask *listReleasesDataTask = [URLSession + dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + // TODO: Reformat error into error code + completion(nil, error); + return; + } - if (HTTPResponse.statusCode == 200) { - [self handleReleasesAPIResponseWithData:data completion:completion]; - } else { - // TODO: Handle non-200 http response - @throw([NSException exceptionWithName:@"NotImplementedException" - reason:@"This code path is not implemented yet" - userInfo:nil]); - } - }]; + NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response; - [listReleasesDataTask resume]; + if (HTTPResponse.statusCode == 200) { + [self handleReleasesAPIResponseWithData:data completion:completion]; + } else { + // TODO: Handle non-200 http response + NSLog(@"ERROR - Non 200 service response - %@", HTTPResponse); + @throw([NSException exceptionWithName:@"NotImplementedException" + reason:@"This code path is not implemented yet" + userInfo:nil]); + } + }]; + + [listReleasesDataTask resume]; + }]; } - (void)handleOauthDiscoveryCompletion:(OIDServiceConfiguration *_Nullable)configuration @@ -172,6 +199,7 @@ NSString *const kAppDistroLibraryName = @"fire-fad"; appDistributionSignInCompletion:(void (^)(NSError *_Nullable error))completion { if (!configuration) { // TODO: Handle when we cannot get configuration + NSLog(@"ERROR - Cannot discover oauth config"); @throw([NSException exceptionWithName:@"NotImplementedException" reason:@"This code path is not implemented yet" userInfo:nil]); @@ -191,19 +219,32 @@ NSString *const kAppDistroLibraryName = @"fire-fad"; additionalParameters:nil]; [self setupUIWindowForLogin]; + + void (^processAuthState)(OIDAuthState *_Nullable authState, NSError *_Nullable error) = ^void( + OIDAuthState *_Nullable authState, NSError *_Nullable error) { + self.authState = authState; + + // Capture errors in persistence but do not bubble them + // up + NSError *authPersistenceError; + if (authState) { + [FIRAppDistributionAuthPersistence persistAuthState:authState error:&authPersistenceError]; + } + + // TODO (schnecle): Log errors in persistence using + // FIRLogger + if (authPersistenceError) { + NSLog(@"Error persisting token to keychain: %@", [error localizedDescription]); + } + self.isTesterSignedIn = self.authState ? YES : NO; + completion(error); + }; + // performs authentication request [FIRAppDistributionAppDelegatorInterceptor sharedInstance].currentAuthorizationFlow = [OIDAuthState authStateByPresentingAuthorizationRequest:request presentingViewController:self.safariHostingViewController - callback:^(OIDAuthState *_Nullable authState, - NSError *_Nullable error) { - [self cleanupUIWindow]; - - self.authState = authState; - self.isTesterSignedIn = - self.authState ? YES : NO; - completion(error); - }]; + callback:processAuthState]; } - (void)setupUIWindowForLogin { diff --git a/FirebaseAppDistribution/Sources/FIRAppDistributionAuthPersistence.m b/FirebaseAppDistribution/Sources/FIRAppDistributionAuthPersistence.m new file mode 100644 index 0000000000..a239aeddd0 --- /dev/null +++ b/FirebaseAppDistribution/Sources/FIRAppDistributionAuthPersistence.m @@ -0,0 +1,122 @@ +// Copyright 2020 Google LLC +// +// 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 "FIRAppDistributionAuthPersistence+Private.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *const kFIRAppDistributionAuthPersistenceErrorDomain = + @"com.firebase.app_distribution.auth_persistence"; + +@implementation FIRAppDistributionAuthPersistence + ++ (void)handleAuthStateError:(NSError **_Nullable)error + description:(NSString *)description + code:(FIRAppDistributionKeychainError)code { + if (error) { + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : description}; + *error = [NSError errorWithDomain:kFIRAppDistributionAuthPersistenceErrorDomain + code:code + userInfo:userInfo]; + } +} + ++ (BOOL)clearAuthState:(NSError **_Nullable)error { + NSMutableDictionary *keychainQuery = [self getKeyChainQuery]; + BOOL success = [FIRAppDistributionKeychainUtility deleteKeychainItem:keychainQuery]; + + if (!success) { + NSString *description = NSLocalizedString( + @"Failed to clear auth state from keychain. Tester will overwrite data on sign in.", + @"Error message for failure to retrieve auth state from keychain"); + [self handleAuthStateError:error + description:description + code:FIRAppDistributionErrorTokenDeletionFailure]; + return NO; + } + + return YES; +} + ++ (OIDAuthState *)retrieveAuthState:(NSError **_Nullable)error { + NSMutableDictionary *keychainQuery = [self getKeyChainQuery]; + [keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData]; + [keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit]; + NSData *passwordData = [FIRAppDistributionKeychainUtility fetchKeychainItemMatching:keychainQuery + error:NULL]; + NSData *result = nil; + + if (!passwordData) { + NSString *description = NSLocalizedString( + @"Failed to retrieve auth state from keychain. Tester will have to sign in again.", + @"Error message for failure to retrieve auth state from keychain"); + [self handleAuthStateError:error + description:description + code:FIRAppDistributionErrorTokenRetrievalFailure]; + return nil; + } + + result = [passwordData copy]; + + if (!result) { + NSString *description = + NSLocalizedString(@"Failed to unarchive auth state. Tester will have to sign in again.", + @"Error message for failure to retrieve auth state from keychain"); + [self handleAuthStateError:error + description:description + code:FIRAppDistributionErrorTokenRetrievalFailure]; + return nil; + } + + OIDAuthState *authState = [FIRAppDistributionKeychainUtility unarchiveKeychainResult:result]; + + return authState; +} + ++ (BOOL)persistAuthState:(OIDAuthState *)authState error:(NSError **_Nullable)error { + NSData *authorizationData = [FIRAppDistributionKeychainUtility archiveDataForKeychain:authState]; + NSMutableDictionary *keychainQuery = [self getKeyChainQuery]; + BOOL success = NO; + BOOL hasAuthState = [self retrieveAuthState:NULL]; + if (hasAuthState) { + success = [FIRAppDistributionKeychainUtility updateKeychainItem:keychainQuery + withDataDictionary:authorizationData]; + } else { + success = [FIRAppDistributionKeychainUtility addKeychainItem:keychainQuery + withDataDictionary:authorizationData]; + } + + if (!success) { + NSString *description = NSLocalizedString( + @"Failed to persist auth state. Tester will have to sign in again after app close.", + @"Error message for failure to persist auth state to keychain"); + [self handleAuthStateError:error + description:description + code:FIRAppDistributionErrorTokenPersistenceFailure]; + return NO; + } + + return YES; +} + ++ (NSMutableDictionary *)getKeyChainQuery { + NSMutableDictionary *keychainQuery = [NSMutableDictionary + dictionaryWithObjectsAndKeys:(id)kSecClassGenericPassword, (id)kSecClass, @"OAuth", + (id)kSecAttrGeneric, @"OAuth", (id)kSecAttrAccount, + @"fire-fad-auth", (id)kSecAttrService, nil]; + return keychainQuery; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppDistribution/Sources/FIRAppDistributionKeychainUtility.m b/FirebaseAppDistribution/Sources/FIRAppDistributionKeychainUtility.m new file mode 100644 index 0000000000..b9b8747fd1 --- /dev/null +++ b/FirebaseAppDistribution/Sources/FIRAppDistributionKeychainUtility.m @@ -0,0 +1,79 @@ +// Copyright 2020 Google LLC +// +// 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 "FIRAppDistributionKeychainUtility+Private.h" + +NSString *const kFIRAppDistributionKeychainErrorDomain = @"com.firebase.app_distribution.keychain"; + +@implementation FIRAppDistributionKeychainUtility + ++ (void)handleAuthStateError:(NSError **_Nullable)error + description:(NSString *)description + code:(int)code { + if (error) { + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : description}; + *error = [NSError errorWithDomain:kFIRAppDistributionKeychainErrorDomain + code:code + userInfo:userInfo]; + } +} + ++ (BOOL)addKeychainItem:(nonnull NSMutableDictionary *)keychainQuery + withDataDictionary:(nonnull NSData *)data { + [keychainQuery setObject:data forKey:(id)kSecValueData]; + OSStatus status = SecItemAdd((CFDictionaryRef)keychainQuery, NULL); + + return status == noErr ? YES : NO; +} + ++ (BOOL)updateKeychainItem:(nonnull NSMutableDictionary *)keychainQuery + withDataDictionary:(nonnull NSData *)data { + OSStatus status = + SecItemUpdate((CFDictionaryRef)keychainQuery, (CFDictionaryRef) @{(id)kSecValueData : data}); + return status == noErr ? YES : NO; +} + ++ (BOOL)deleteKeychainItem:(nonnull NSMutableDictionary *)keychainQuery { + OSStatus status = SecItemDelete((CFDictionaryRef)keychainQuery); + + return status != errSecSuccess && status != errSecItemNotFound ? NO : YES; +} + ++ (NSData *)fetchKeychainItemMatching:(nonnull NSMutableDictionary *)keychainQuery + error:(NSError **_Nullable)error { + NSData *keychainItem; + OSStatus status = SecItemCopyMatching((CFDictionaryRef)keychainQuery, (void *)&keychainItem); + + if (status != noErr || 0 == [keychainItem length]) { + if (error) { + NSString *description = + NSLocalizedString(@"Failed to fetch keychain item.", + @"Error message for failure to retrieve auth state from keychain"); + [self handleAuthStateError:error description:description code:0]; + return nil; + } + } + + return keychainItem; +} + ++ (OIDAuthState *)unarchiveKeychainResult:(NSData *)result { + return (OIDAuthState *)[NSKeyedUnarchiver unarchiveObjectWithData:result]; +} + ++ (NSData *)archiveDataForKeychain:(OIDAuthState *)data { + return [NSKeyedArchiver archivedDataWithRootObject:data]; +} + +@end diff --git a/FirebaseAppDistribution/Sources/FIRAppDistributionMachO.m b/FirebaseAppDistribution/Sources/FIRAppDistributionMachO.m index a0f4106ed1..d582a08ceb 100644 --- a/FirebaseAppDistribution/Sources/FIRAppDistributionMachO.m +++ b/FirebaseAppDistribution/Sources/FIRAppDistributionMachO.m @@ -36,7 +36,6 @@ _slices = [NSMutableArray new]; [self extractSlices]; } - return self; } diff --git a/FirebaseAppDistribution/Sources/Private/FIRAppDistributionAuthPersistence+Private.h b/FirebaseAppDistribution/Sources/Private/FIRAppDistributionAuthPersistence+Private.h new file mode 100644 index 0000000000..4a14d45162 --- /dev/null +++ b/FirebaseAppDistribution/Sources/Private/FIRAppDistributionAuthPersistence+Private.h @@ -0,0 +1,55 @@ +// Copyright 2020 Google LLC +// +// 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 "FIRAppDistributionKeychainUtility+Private.h" + +NS_ASSUME_NONNULL_BEGIN + +// Label exceptions from AppDistributionAuthPersistence calls. +FOUNDATION_EXPORT NSString *const kFIRAppDistributionAuthPersistenceErrorDomain; + +/** + * The set of error codes that may be returned from internal calls to persist Tester authentication + * to the keychain. These should never be returned to the user. + * @enum FIRAppDistributionKeychainError + */ +typedef NS_ENUM(NSUInteger, FIRAppDistributionKeychainError) { + // Authentication token persistence error + FIRAppDistributionErrorTokenPersistenceFailure = 0, + + // Authentication token retrieval error + FIRAppDistributionErrorTokenRetrievalFailure = 1, + + // Authentication token deletion error + FIRAppDistributionErrorTokenDeletionFailure = 2, +} NS_SWIFT_NAME(AppDistributionKeychainError); + +@interface FIRAppDistributionAuthPersistence : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +// Handle null checking, creation, and formatting of an error encountered ++ (void)handleAuthStateError:(NSError **_Nullable)error + description:(NSString *)description + code:(FIRAppDistributionKeychainError)code; + ++ (BOOL)persistAuthState:(OIDAuthState *)authState error:(NSError **_Nullable)error; + ++ (BOOL)clearAuthState:(NSError **_Nullable)error; + ++ (OIDAuthState *)retrieveAuthState:(NSError **_Nullable)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppDistribution/Sources/Private/FIRAppDistributionKeychainUtility+Private.h b/FirebaseAppDistribution/Sources/Private/FIRAppDistributionKeychainUtility+Private.h new file mode 100644 index 0000000000..1eb3ed1684 --- /dev/null +++ b/FirebaseAppDistribution/Sources/Private/FIRAppDistributionKeychainUtility+Private.h @@ -0,0 +1,41 @@ +// Copyright 2020 Google LLC +// +// 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 + +NS_ASSUME_NONNULL_BEGIN + +/// @brief Wraps keychain operations to encapsulate interactions with CF data structures +@interface FIRAppDistributionKeychainUtility : NSObject + +/// @brief Store an item in the keychain ++ (BOOL)addKeychainItem:(NSMutableDictionary *)keychainQuery withDataDictionary:(NSData *)data; + +/// @brief Update an item in the keychain ++ (BOOL)updateKeychainItem:(NSMutableDictionary *)keychainQuery withDataDictionary:(NSData *)data; + +/// @brief Delete an item in the keychain ++ (BOOL)deleteKeychainItem:(NSMutableDictionary *)keychainQuery; + +/// @brief Fetch the item matching the keychain query from the keychain ++ (NSData *)fetchKeychainItemMatching:(nonnull NSMutableDictionary *)keychainQuery + error:(NSError **_Nullable)error; + +/// @brief Unarchive the authentication state from the keychain result ++ (OIDAuthState *)unarchiveKeychainResult:(NSData *)result; + +/// @brief Archive the authentication data for persistence to the keychain ++ (NSData *)archiveDataForKeychain:(OIDAuthState *)data; +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseAppDistribution/Tests/Unit/FIRAppDistributionAuthPersistenceTests.m b/FirebaseAppDistribution/Tests/Unit/FIRAppDistributionAuthPersistenceTests.m new file mode 100644 index 0000000000..7b78d80229 --- /dev/null +++ b/FirebaseAppDistribution/Tests/Unit/FIRAppDistributionAuthPersistenceTests.m @@ -0,0 +1,137 @@ +// +// Copyright 2020 Google LLC +// +// 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 +#import + +@interface FIRAppDistributionAuthPersistenceTests : XCTestCase +@end + +@implementation FIRAppDistributionAuthPersistenceTests { + NSMutableDictionary *_mockKeychainQuery; + id _mockAuthorizationData; + id _mockOIDAuthState; + id _mockKeychainUtility; + id _partialMockAuthPersitence; +} + +- (void)setUp { + [super setUp]; + _mockKeychainQuery = [NSMutableDictionary + dictionaryWithObjectsAndKeys:(id) @"thing one", (id) @"another thing", nil]; + _mockKeychainUtility = OCMClassMock([FIRAppDistributionKeychainUtility class]); + _mockAuthorizationData = [@"this is some password stuff" dataUsingEncoding:NSUTF8StringEncoding]; + _mockOIDAuthState = OCMClassMock([OIDAuthState class]); + OCMStub(ClassMethod([_mockKeychainUtility unarchiveKeychainResult:[OCMArg any]])) + .andReturn(_mockOIDAuthState); + OCMStub(ClassMethod([_mockKeychainUtility archiveDataForKeychain:[OCMArg any]])) + .andReturn(_mockAuthorizationData); +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testPersistAuthStateSuccess { + OCMStub(ClassMethod([_mockKeychainUtility addKeychainItem:[OCMArg any] + withDataDictionary:[OCMArg any]])) + .andReturn(YES); + NSError *error; + XCTAssertTrue([FIRAppDistributionAuthPersistence persistAuthState:_mockOIDAuthState + error:&error]); + XCTAssertNil(error); +} + +- (void)testPersistAuthStateFailure { + OCMStub(ClassMethod([_mockKeychainUtility addKeychainItem:[OCMArg any] + withDataDictionary:[OCMArg any]])) + .andReturn(NO); + NSError *error; + XCTAssertFalse([FIRAppDistributionAuthPersistence persistAuthState:_mockOIDAuthState + error:&error]); + XCTAssertNotNil(error); + XCTAssertEqual([error domain], kFIRAppDistributionAuthPersistenceErrorDomain); + XCTAssertEqual([error code], FIRAppDistributionErrorTokenPersistenceFailure); +} + +- (void)testOverwriteAuthStateSuccess { + OCMStub(ClassMethod([_mockKeychainUtility fetchKeychainItemMatching:[OCMArg any] + error:[OCMArg setTo:nil]])) + .andReturn(_mockAuthorizationData); + OCMStub(ClassMethod([_mockKeychainUtility updateKeychainItem:[OCMArg any] + withDataDictionary:[OCMArg any]])) + .andReturn(YES); + NSError *error; + XCTAssertTrue([FIRAppDistributionAuthPersistence persistAuthState:_mockOIDAuthState + error:&error]); + XCTAssertNil(error); +} + +- (void)testOverwriteAuthStateFailure { + OCMStub(ClassMethod([_mockKeychainUtility fetchKeychainItemMatching:[OCMArg any] + error:[OCMArg setTo:nil]])) + .andReturn(_mockAuthorizationData); + OCMStub(ClassMethod([_mockKeychainUtility updateKeychainItem:[OCMArg any] + withDataDictionary:[OCMArg any]])) + .andReturn(NO); + NSError *error; + XCTAssertFalse([FIRAppDistributionAuthPersistence persistAuthState:_mockOIDAuthState + error:&error]); + XCTAssertNotNil(error); + XCTAssertEqual([error domain], kFIRAppDistributionAuthPersistenceErrorDomain); + XCTAssertEqual([error code], FIRAppDistributionErrorTokenPersistenceFailure); +} + +- (void)testRetrieveAuthStateSuccess { + OCMStub(ClassMethod([_mockKeychainUtility fetchKeychainItemMatching:[OCMArg any] + error:[OCMArg setTo:nil]])) + .andReturn(_mockAuthorizationData); + NSError *error; + XCTAssertTrue([[FIRAppDistributionAuthPersistence retrieveAuthState:&error] + isKindOfClass:[OIDAuthState class]]); + XCTAssertNil(error); +} + +- (void)testRetrieveAuthStateFailure { + OCMStub(ClassMethod([_mockKeychainUtility fetchKeychainItemMatching:[OCMArg any] + error:[OCMArg setTo:nil]])) + .andReturn(nil); + NSError *error; + XCTAssertFalse([FIRAppDistributionAuthPersistence retrieveAuthState:&error]); + XCTAssertNotNil(error); + XCTAssertEqual([error domain], kFIRAppDistributionAuthPersistenceErrorDomain); + XCTAssertEqual([error code], FIRAppDistributionErrorTokenRetrievalFailure); +} + +- (void)testClearAuthStateSuccess { + OCMStub(ClassMethod([_mockKeychainUtility deleteKeychainItem:[OCMArg any]])).andReturn(YES); + NSError *error; + XCTAssertTrue([FIRAppDistributionAuthPersistence clearAuthState:&error]); + XCTAssertNil(error); +} + +- (void)testClearAuthStateFailure { + OCMStub(ClassMethod([_mockKeychainUtility deleteKeychainItem:[OCMArg any]])).andReturn(NO); + NSError *error; + XCTAssertFalse([FIRAppDistributionAuthPersistence clearAuthState:&error]); + XCTAssertNotNil(error); + XCTAssertEqual([error domain], kFIRAppDistributionAuthPersistenceErrorDomain); + XCTAssertEqual([error code], FIRAppDistributionErrorTokenDeletionFailure); +} + +@end diff --git a/FirebaseAppDistribution/Tests/Unit/FIRAppDistributionTests.m b/FirebaseAppDistribution/Tests/Unit/FIRAppDistributionTests.m index 1dcdaabafe..a6fd7a7e97 100644 --- a/FirebaseAppDistribution/Tests/Unit/FIRAppDistributionTests.m +++ b/FirebaseAppDistribution/Tests/Unit/FIRAppDistributionTests.m @@ -16,7 +16,7 @@ #import #import -#import "FIRAppDistribution.h" +#import "FirebaseAppDistribution/FIRAppDistribution.h" @interface FIRAppDistributionSampleTests : XCTestCase