From f9d7ab9b230c0003aca2cfcde6605e3850581f52 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Fri, 7 Jun 2024 18:10:18 -0400 Subject: [PATCH 1/7] [v8] Replace read keychain API with completion handler based API (#182) --- .../GoogleUtilities/GULKeychainStorage.h | 25 ++--- .../SecureStorage/GULKeychainStorage.m | 102 +++++++++--------- 2 files changed, 62 insertions(+), 65 deletions(-) diff --git a/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h b/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h index af10cb4d..879e5460 100644 --- a/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h +++ b/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h @@ -34,18 +34,19 @@ NS_ASSUME_NONNULL_BEGIN */ - (instancetype)initWithService:(NSString *)service; -/** - * Get an object by key. - * @param key The key. - * @param objectClass The expected object class required by `NSSecureCoding`. - * @param accessGroup The Keychain Access Group. - * - * @return Returns a promise. It is resolved with an object stored by key if exists. It is resolved - * with `nil` when the object not found. It fails on a Keychain error. - */ -- (FBLPromise> *)getObjectForKey:(NSString *)key - objectClass:(Class)objectClass - accessGroup:(nullable NSString *)accessGroup; +/// Get an object by key. +/// @param key The key. +/// @param objectClass The expected object class required by `NSSecureCoding`. +/// @param accessGroup The Keychain Access Group. +/// @param completionHandler The completion handler to call when the +/// synchronized keychain read is complete. An error is passed to the +/// completion handler if the keychain read fails. Else, the object stored in +/// the keychain, or `nil` if it does not exist, is passed to the completion +/// handler. +- (void)getObjectForKey:(NSString *)key + objectClass:(Class)objectClass + accessGroup:(nullable NSString *)accessGroup + completionHandler:(void (^)(id _Nullable obj, NSError * _Nullable error))completionHandler; /** * Saves the given object by the given key. diff --git a/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m b/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m index 2b5cc8a3..df0ad98c 100644 --- a/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m +++ b/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m @@ -56,25 +56,23 @@ - (instancetype)initWithService:(NSString *)service cache:(NSCache *)cache { #pragma mark - Public -- (FBLPromise> *)getObjectForKey:(NSString *)key - objectClass:(Class)objectClass - accessGroup:(nullable NSString *)accessGroup { - return [FBLPromise onQueue:self.inMemoryCacheQueue - do:^id _Nullable { - // Return cached object or fail otherwise. - id object = [self.inMemoryCache objectForKey:key]; - return object - ?: [[NSError alloc] - initWithDomain:FBLPromiseErrorDomain - code:FBLPromiseErrorCodeValidationFailure - userInfo:nil]; - }] - .recover(^id _Nullable(NSError *error) { - // Look for the object in the keychain. - return [self getObjectFromKeychainForKey:key - objectClass:objectClass - accessGroup:accessGroup]; - }); +- (void)getObjectForKey:(NSString *)key + objectClass:(Class)objectClass + accessGroup:(nullable NSString *)accessGroup + completionHandler:(void (^)(id _Nullable obj, NSError * _Nullable error))completionHandler { + dispatch_async(self.inMemoryCacheQueue, ^{ + // Return cached object or fail otherwise. + id object = [self.inMemoryCache objectForKey:key]; + if (object) { + completionHandler(object, nil); + } else { + // Look for the object in the keychain. + [self getObjectFromKeychainForKey:key + objectClass:objectClass + accessGroup:accessGroup + completionHandler:completionHandler]; + } + }); } - (FBLPromise *)setObject:(id)object @@ -126,40 +124,38 @@ - (instancetype)initWithService:(NSString *)service cache:(NSCache *)cache { #pragma mark - Private -- (FBLPromise> *)getObjectFromKeychainForKey:(NSString *)key - objectClass:(Class)objectClass - accessGroup:(nullable NSString *)accessGroup { - // Look for the object in the keychain. - return [FBLPromise - onQueue:self.keychainQueue - do:^id { - NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup]; - NSError *error; - NSData *encodedObject = [GULKeychainUtils getItemWithQuery:query error:&error]; - - if (error) { - return error; - } - if (!encodedObject) { - return nil; - } - id object = [NSKeyedUnarchiver unarchivedObjectOfClass:objectClass - fromData:encodedObject - error:&error]; - if (error) { - return error; - } - - return object; - }] - .thenOn(self.inMemoryCacheQueue, - ^id _Nullable(id _Nullable object) { - // Save object to the in-memory cache if exists and return the object. - if (object) { - [self.inMemoryCache setObject:object forKey:[key copy]]; - } - return object; - }); +- (void)getObjectFromKeychainForKey:(NSString *)key + objectClass:(Class)objectClass + accessGroup:(nullable NSString *)accessGroup + completionHandler:(void (^)(id _Nullable obj, NSError * _Nullable error))completionHandler { + // Look for the object in the keychain. + dispatch_async(self.keychainQueue, ^{ + NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup]; + NSError *error; + NSData *encodedObject = [GULKeychainUtils getItemWithQuery:query error:&error]; + + if (error) { + completionHandler(nil, error); + } + if (!encodedObject) { + completionHandler(nil, nil); + } + id object = [NSKeyedUnarchiver unarchivedObjectOfClass:objectClass + fromData:encodedObject + error:&error]; + if (error) { + completionHandler(nil, error); + } + + dispatch_async(self.inMemoryCacheQueue, ^{ + // Save object to the in-memory cache if exists and return the object. + if (object) { + [self.inMemoryCache setObject:object forKey:[key copy]]; + } + + completionHandler(object, nil); + }); + }); } - (void)resetInMemoryCache { From 79655ef12f3f25654c89666dc724c97ef048668d Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Fri, 7 Jun 2024 19:23:09 -0400 Subject: [PATCH 2/7] [v8] Replace write keychain API with completion handler based API (#183) --- .../GoogleUtilities/GULKeychainStorage.h | 21 ++++---- .../SecureStorage/GULKeychainStorage.m | 53 ++++++++++--------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h b/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h index 879e5460..e8ab7c2c 100644 --- a/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h +++ b/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h @@ -48,18 +48,15 @@ NS_ASSUME_NONNULL_BEGIN accessGroup:(nullable NSString *)accessGroup completionHandler:(void (^)(id _Nullable obj, NSError * _Nullable error))completionHandler; -/** - * Saves the given object by the given key. - * @param object The object to store. - * @param key The key to store the object. If there is an existing object by the key, it will be - * overridden. - * @param accessGroup The Keychain Access Group. - * - * @return Returns which is resolved with `[NSNull null]` on success. - */ -- (FBLPromise *)setObject:(id)object - forKey:(NSString *)key - accessGroup:(nullable NSString *)accessGroup; +/// Saves the given object by the given key. +/// @param object The object to store. +/// @param key The key to store the object. If there is an existing object by the key, it will be overridden. +/// @param accessGroup The Keychain Access Group. +/// @param completionHandler Returns the object that was successfully stored, or an error if one occurred. +- (void)setObject:(id)object + forKey:(NSString *)key + accessGroup:(nullable NSString *)accessGroup +completionHandler:(void (^)(id _Nullable obj, NSError * _Nullable error))completionHandler; /** * Removes the object by the given key. diff --git a/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m b/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m index df0ad98c..d24d9f40 100644 --- a/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m +++ b/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m @@ -75,32 +75,34 @@ - (void)getObjectForKey:(NSString *)key }); } -- (FBLPromise *)setObject:(id)object - forKey:(NSString *)key - accessGroup:(nullable NSString *)accessGroup { - return [FBLPromise onQueue:self.inMemoryCacheQueue - do:^id _Nullable { - // Save to the in-memory cache first. - [self.inMemoryCache setObject:object forKey:[key copy]]; - return [NSNull null]; - }] - .thenOn(self.keychainQueue, ^id(id result) { - // Then store the object to the keychain. - NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup]; - NSError *error; - NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object - requiringSecureCoding:YES - error:&error]; - if (!encodedObject) { - return error; - } +- (void)setObject:(id)object + forKey:(NSString *)key + accessGroup:(nullable NSString *)accessGroup +completionHandler:(void (^)(id _Nullable obj, NSError * _Nullable error))completionHandler { + dispatch_async(self.inMemoryCacheQueue, ^{ + // Save to the in-memory cache first. + [self.inMemoryCache setObject:object forKey:[key copy]]; + + dispatch_async(self.keychainQueue, ^{ + // Then store the object to the keychain. + NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup]; + NSError *error; + NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object + requiringSecureCoding:YES + error:&error]; + if (!encodedObject) { + completionHandler(nil, error); + return; + } - if (![GULKeychainUtils setItem:encodedObject withQuery:query error:&error]) { - return error; - } + if (![GULKeychainUtils setItem:encodedObject withQuery:query error:&error]) { + completionHandler(nil, error); + return; + } - return [NSNull null]; - }); + completionHandler(object, nil); + }); + }); } - (FBLPromise *)removeObjectForKey:(NSString *)key @@ -136,15 +138,18 @@ - (void)getObjectFromKeychainForKey:(NSString *)key if (error) { completionHandler(nil, error); + return; } if (!encodedObject) { completionHandler(nil, nil); + return; } id object = [NSKeyedUnarchiver unarchivedObjectOfClass:objectClass fromData:encodedObject error:&error]; if (error) { completionHandler(nil, error); + return; } dispatch_async(self.inMemoryCacheQueue, ^{ From d0b6e203a5cdeae9547d8af47466c3cfcc5c464f Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:02:23 -0400 Subject: [PATCH 3/7] [v8] Replace remove keychain API with completion handler based API (#184) * [v8] Replace remove keychain API with completion handler based API * Update GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m --- .../GoogleUtilities/GULKeychainStorage.h | 23 +++++++------ .../SecureStorage/GULKeychainStorage.m | 32 +++++++++---------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h b/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h index e8ab7c2c..7b992085 100644 --- a/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h +++ b/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h @@ -58,16 +58,19 @@ NS_ASSUME_NONNULL_BEGIN accessGroup:(nullable NSString *)accessGroup completionHandler:(void (^)(id _Nullable obj, NSError * _Nullable error))completionHandler; -/** - * Removes the object by the given key. - * @param key The key to store the object. If there is an existing object by the key, it will be - * overridden. - * @param accessGroup The Keychain Access Group. - * - * @return Returns which is resolved with `[NSNull null]` on success. - */ -- (FBLPromise *)removeObjectForKey:(NSString *)key - accessGroup:(nullable NSString *)accessGroup; +/// Removes the object by the given key. +/// @param key The key to store the object. If there is an existing object by +/// the key, it will be overridden. +/// @param accessGroup The Keychain Access Group. +/// @param completionHandler The completion handler to call when the +/// synchronized keychain removal is complete. An error is passed to the +/// completion handler if the keychain removal fails. Else, `YES` if the item +/// was removed successfully or doesn’t exist, `NO` otherwise. +/// @note In the event an error occurs, the completion handler will return an +/// error and a boolean to indicate the removal failed (`NO`). +- (void)removeObjectForKey:(NSString *)key + accessGroup:(nullable NSString *)accessGroup + completionHandler:(void (^)(BOOL success, NSError * _Nullable error))completionHandler; #if TARGET_OS_OSX /// If not `nil`, then only this keychain will be used to save and read data (see diff --git a/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m b/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m index d24d9f40..99c5c782 100644 --- a/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m +++ b/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m @@ -105,23 +105,23 @@ - (void)setObject:(id)object }); } -- (FBLPromise *)removeObjectForKey:(NSString *)key - accessGroup:(nullable NSString *)accessGroup { - return [FBLPromise onQueue:self.inMemoryCacheQueue - do:^id _Nullable { - [self.inMemoryCache removeObjectForKey:key]; - return nil; - }] - .thenOn(self.keychainQueue, ^id(id result) { - NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup]; - - NSError *error; - if (![GULKeychainUtils removeItemWithQuery:query error:&error]) { - return error; - } +- (void)removeObjectForKey:(NSString *)key + accessGroup:(nullable NSString *)accessGroup + completionHandler:(void (^)(BOOL success, NSError * _Nullable error))completionHandler { + dispatch_async(self.inMemoryCacheQueue, ^{ + [self.inMemoryCache removeObjectForKey:key]; + dispatch_async(self.keychainQueue, ^{ + NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup]; - return [NSNull null]; - }); + NSError *error; + BOOL success = [GULKeychainUtils removeItemWithQuery:query error:&error]; + if (!success) { + completionHandler(NO, error); + return; + } + completionHandler(success, nil); + }); + }); } #pragma mark - Private From 349a3aa05ad9b49a4a2b8dcecd402b48eecdf40f Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 10 Jun 2024 21:40:09 -0400 Subject: [PATCH 4/7] [v8] Fix tests, docs, and imports following `GULKeychainStorage` API changes (#185) --- .../GoogleUtilities/GULKeychainStorage.h | 24 ++- .../SecureStorage/GULKeychainStorage.m | 179 +++++++++--------- .../Environment/GULKeychainStorageTests.m | 110 +++++++---- 3 files changed, 172 insertions(+), 141 deletions(-) diff --git a/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h b/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h index 7b992085..92870590 100644 --- a/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h +++ b/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h @@ -16,8 +16,6 @@ #import -@class FBLPromise; - NS_ASSUME_NONNULL_BEGIN /// The class provides a convenient, multiplatform abstraction of the Keychain. @@ -46,17 +44,23 @@ NS_ASSUME_NONNULL_BEGIN - (void)getObjectForKey:(NSString *)key objectClass:(Class)objectClass accessGroup:(nullable NSString *)accessGroup - completionHandler:(void (^)(id _Nullable obj, NSError * _Nullable error))completionHandler; + completionHandler: + (void (^)(id _Nullable obj, NSError *_Nullable error))completionHandler; /// Saves the given object by the given key. /// @param object The object to store. -/// @param key The key to store the object. If there is an existing object by the key, it will be overridden. +/// @param key The key to store the object. If there is an existing object by the key, it will be +/// overridden. /// @param accessGroup The Keychain Access Group. -/// @param completionHandler Returns the object that was successfully stored, or an error if one occurred. +/// @param completionHandler The completion handler to call when the +/// synchronized keychain write is complete. An error is passed to the +/// completion handler if the keychain read fails. Else, the object written to +/// the keychain is passed to the completion handler. - (void)setObject:(id)object - forKey:(NSString *)key - accessGroup:(nullable NSString *)accessGroup -completionHandler:(void (^)(id _Nullable obj, NSError * _Nullable error))completionHandler; + forKey:(NSString *)key + accessGroup:(nullable NSString *)accessGroup + completionHandler: + (void (^)(id _Nullable obj, NSError *_Nullable error))completionHandler; /// Removes the object by the given key. /// @param key The key to store the object. If there is an existing object by @@ -69,8 +73,8 @@ completionHandler:(void (^)(id _Nullable obj, NSError * _Nullabl /// @note In the event an error occurs, the completion handler will return an /// error and a boolean to indicate the removal failed (`NO`). - (void)removeObjectForKey:(NSString *)key - accessGroup:(nullable NSString *)accessGroup - completionHandler:(void (^)(BOOL success, NSError * _Nullable error))completionHandler; + accessGroup:(nullable NSString *)accessGroup + completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler; #if TARGET_OS_OSX /// If not `nil`, then only this keychain will be used to save and read data (see diff --git a/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m b/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m index 99c5c782..0ddb79ea 100644 --- a/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m +++ b/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m @@ -17,12 +17,6 @@ #import "GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h" #import -#if __has_include() -#import -#else -#import "FBLPromises.h" -#endif - #import "GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainUtils.h" @interface GULKeychainStorage () @@ -59,69 +53,71 @@ - (instancetype)initWithService:(NSString *)service cache:(NSCache *)cache { - (void)getObjectForKey:(NSString *)key objectClass:(Class)objectClass accessGroup:(nullable NSString *)accessGroup - completionHandler:(void (^)(id _Nullable obj, NSError * _Nullable error))completionHandler { - dispatch_async(self.inMemoryCacheQueue, ^{ - // Return cached object or fail otherwise. - id object = [self.inMemoryCache objectForKey:key]; - if (object) { - completionHandler(object, nil); - } else { - // Look for the object in the keychain. - [self getObjectFromKeychainForKey:key - objectClass:objectClass - accessGroup:accessGroup - completionHandler:completionHandler]; - } - }); + completionHandler: + (void (^)(id _Nullable obj, NSError *_Nullable error))completionHandler { + dispatch_async(self.inMemoryCacheQueue, ^{ + // Return cached object or fail otherwise. + id object = [self.inMemoryCache objectForKey:key]; + if (object) { + completionHandler(object, nil); + } else { + // Look for the object in the keychain. + [self getObjectFromKeychainForKey:key + objectClass:objectClass + accessGroup:accessGroup + completionHandler:completionHandler]; + } + }); } - (void)setObject:(id)object - forKey:(NSString *)key - accessGroup:(nullable NSString *)accessGroup -completionHandler:(void (^)(id _Nullable obj, NSError * _Nullable error))completionHandler { - dispatch_async(self.inMemoryCacheQueue, ^{ - // Save to the in-memory cache first. - [self.inMemoryCache setObject:object forKey:[key copy]]; + forKey:(NSString *)key + accessGroup:(nullable NSString *)accessGroup + completionHandler: + (void (^)(id _Nullable obj, NSError *_Nullable error))completionHandler { + dispatch_async(self.inMemoryCacheQueue, ^{ + // Save to the in-memory cache first. + [self.inMemoryCache setObject:object forKey:[key copy]]; - dispatch_async(self.keychainQueue, ^{ - // Then store the object to the keychain. - NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup]; - NSError *error; - NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object - requiringSecureCoding:YES - error:&error]; - if (!encodedObject) { - completionHandler(nil, error); - return; - } - - if (![GULKeychainUtils setItem:encodedObject withQuery:query error:&error]) { - completionHandler(nil, error); - return; - } - - completionHandler(object, nil); - }); + dispatch_async(self.keychainQueue, ^{ + // Then store the object to the keychain. + NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup]; + NSError *error; + NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object + requiringSecureCoding:YES + error:&error]; + if (!encodedObject) { + completionHandler(nil, error); + return; + } + + if (![GULKeychainUtils setItem:encodedObject withQuery:query error:&error]) { + completionHandler(nil, error); + return; + } + + completionHandler(object, nil); }); + }); } - (void)removeObjectForKey:(NSString *)key - accessGroup:(nullable NSString *)accessGroup - completionHandler:(void (^)(BOOL success, NSError * _Nullable error))completionHandler { - dispatch_async(self.inMemoryCacheQueue, ^{ - [self.inMemoryCache removeObjectForKey:key]; - dispatch_async(self.keychainQueue, ^{ - NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup]; - - NSError *error; - BOOL success = [GULKeychainUtils removeItemWithQuery:query error:&error]; - if (!success) { - completionHandler(NO, error); - return; - } - completionHandler(success, nil); - }); + accessGroup:(nullable NSString *)accessGroup + completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler { + dispatch_async(self.inMemoryCacheQueue, ^{ + [self.inMemoryCache removeObjectForKey:key]; + dispatch_async(self.keychainQueue, ^{ + NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup]; + + NSError *error; + BOOL success = [GULKeychainUtils removeItemWithQuery:query error:&error]; + if (!success) { + completionHandler(NO, error); + return; + } + completionHandler(success, nil); }); + }); } #pragma mark - Private @@ -129,38 +125,39 @@ - (void)removeObjectForKey:(NSString *)key - (void)getObjectFromKeychainForKey:(NSString *)key objectClass:(Class)objectClass accessGroup:(nullable NSString *)accessGroup - completionHandler:(void (^)(id _Nullable obj, NSError * _Nullable error))completionHandler { - // Look for the object in the keychain. - dispatch_async(self.keychainQueue, ^{ - NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup]; - NSError *error; - NSData *encodedObject = [GULKeychainUtils getItemWithQuery:query error:&error]; - - if (error) { - completionHandler(nil, error); - return; - } - if (!encodedObject) { - completionHandler(nil, nil); - return; - } - id object = [NSKeyedUnarchiver unarchivedObjectOfClass:objectClass - fromData:encodedObject - error:&error]; - if (error) { - completionHandler(nil, error); - return; - } - - dispatch_async(self.inMemoryCacheQueue, ^{ - // Save object to the in-memory cache if exists and return the object. - if (object) { - [self.inMemoryCache setObject:object forKey:[key copy]]; - } - - completionHandler(object, nil); - }); + completionHandler:(void (^)(id _Nullable obj, + NSError *_Nullable error))completionHandler { + // Look for the object in the keychain. + dispatch_async(self.keychainQueue, ^{ + NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup]; + NSError *error; + NSData *encodedObject = [GULKeychainUtils getItemWithQuery:query error:&error]; + + if (error) { + completionHandler(nil, error); + return; + } + if (!encodedObject) { + completionHandler(nil, nil); + return; + } + id object = [NSKeyedUnarchiver unarchivedObjectOfClass:objectClass + fromData:encodedObject + error:&error]; + if (error) { + completionHandler(nil, error); + return; + } + + dispatch_async(self.inMemoryCacheQueue, ^{ + // Save object to the in-memory cache if exists and return the object. + if (object) { + [self.inMemoryCache setObject:object forKey:[key copy]]; + } + + completionHandler(object, nil); }); + }); } - (void)resetInMemoryCache { diff --git a/GoogleUtilities/Tests/Unit/Environment/GULKeychainStorageTests.m b/GoogleUtilities/Tests/Unit/Environment/GULKeychainStorageTests.m index d2512974..af37dbde 100644 --- a/GoogleUtilities/Tests/Unit/Environment/GULKeychainStorageTests.m +++ b/GoogleUtilities/Tests/Unit/Environment/GULKeychainStorageTests.m @@ -29,7 +29,6 @@ #import #import -#import "FBLPromise+Testing.h" #import "GoogleUtilities/Tests/Unit/Utils/GULTestKeychain.h" #import "GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h" @@ -120,16 +119,21 @@ - (void)testGetExistingObjectClassMismatch { // Skip in-memory cache because the error is relevant only for Keychain. OCMExpect([self.mockCache objectForKey:key]).andReturn(nil); - FBLPromise> *getPromise = [self.storage getObjectForKey:key - objectClass:[NSString class] - accessGroup:nil]; - - XCTAssert(FBLWaitForPromisesWithTimeout(1)); - XCTAssertNil(getPromise.value); - XCTAssertNotNil(getPromise.error); - // TODO: Test for particular error. - - OCMVerifyAll(self.mockCache); + XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; + [self.storage getObjectForKey:key + objectClass:[NSString class] + accessGroup:nil + completionHandler:^(id _Nullable obj, NSError *_Nullable error) { + XCTAssertNil(obj); + // Assert class mismatch error. + XCTAssertNotNil(error); + XCTAssertEqual(error.domain, NSCocoaErrorDomain); + XCTAssertEqual(error.code, 4864); + + OCMVerifyAll(self.mockCache); + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:1.0]; } - (void)testRemoveExistingObject { @@ -155,15 +159,23 @@ - (void)testRemoveNonExistingObject { - (void)assertSuccessWriteObject:(id)object forKey:(NSString *)key { OCMExpect([self.mockCache setObject:object forKey:key]).andForwardToRealObject(); - FBLPromise *setPromise = [self.storage setObject:object forKey:key accessGroup:nil]; - - XCTAssert(FBLWaitForPromisesWithTimeout(1)); - XCTAssertNil(setPromise.error, @"%@", self.name); - + XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; + __weak __auto_type weakSelf = self; + [self.storage setObject:object + forKey:key + accessGroup:nil + completionHandler:^(id _Nullable obj, NSError *_Nullable error) { + if (!weakSelf) { + return; + } + XCTAssertNil(error, @"%@", weakSelf.name); + // Check in-memory cache. + XCTAssertEqualObjects([weakSelf.cache objectForKey:key], object); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:1.0]; OCMVerifyAll(self.mockCache); - - // Check in-memory cache. - XCTAssertEqualObjects([self.cache objectForKey:key], object); } - (void)assertSuccessReadObject:(id)object @@ -176,39 +188,57 @@ - (void)assertSuccessReadObject:(id)object OCMExpect([self.mockCache setObject:object forKey:key]).andForwardToRealObject(); } - FBLPromise> *getPromise = - [self.storage getObjectForKey:key objectClass:class accessGroup:nil]; - - XCTAssert(FBLWaitForPromisesWithTimeout(1), @"%@", self.name); - XCTAssertEqualObjects(getPromise.value, object, @"%@", self.name); - XCTAssertNil(getPromise.error, @"%@", self.name); - + XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; + __weak __auto_type weakSelf = self; + [self.storage + getObjectForKey:key + objectClass:class + accessGroup:nil + completionHandler:^(id _Nullable obj, NSError *_Nullable error) { + if (!weakSelf) { + return; + } + XCTAssertEqualObjects(obj, object, @"%@", weakSelf.name); + XCTAssertNil(error, @"%@", weakSelf.name); + // Check in-memory cache. + XCTAssertEqualObjects([weakSelf.cache objectForKey:key], object, @"%@", weakSelf.name); + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:1.0]; OCMVerifyAll(self.mockCache); - - // Check in-memory cache. - XCTAssertEqualObjects([self.cache objectForKey:key], object, @"%@", self.name); } - (void)assertNonExistingObjectForKey:(NSString *)key class:(Class)class { OCMExpect([self.mockCache objectForKey:key]).andForwardToRealObject(); - FBLPromise> *promise = - [self.storage getObjectForKey:key objectClass:class accessGroup:nil]; - - XCTAssert(FBLWaitForPromisesWithTimeout(1)); - XCTAssertNil(promise.error, @"%@", self.name); - XCTAssertNil(promise.value, @"%@", self.name); - + XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; + __weak __auto_type weakSelf = self; + [self.storage getObjectForKey:key + objectClass:class + accessGroup:nil + completionHandler:^(id _Nullable obj, NSError *_Nullable error) { + if (!weakSelf) { + return; + } + XCTAssertNil(error, @"%@", weakSelf.name); + XCTAssertNil(obj, @"%@", weakSelf.name); + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:1.0]; OCMVerifyAll(self.mockCache); } - (void)assertRemoveObjectForKey:(NSString *)key { OCMExpect([self.mockCache removeObjectForKey:key]).andForwardToRealObject(); - FBLPromise *removePromise = [self.storage removeObjectForKey:key accessGroup:nil]; - XCTAssert(FBLWaitForPromisesWithTimeout(1)); - XCTAssertNil(removePromise.error); - + XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; + [self.storage removeObjectForKey:key + accessGroup:nil + completionHandler:^(BOOL success, NSError *_Nullable error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:1.0]; OCMVerifyAll(self.mockCache); } From 4995244e7a0fe78e19d2d434927ee9970c4f891e Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:40:14 -0400 Subject: [PATCH 5/7] [v8] Remove Promises API and dependency specifications (#186) * [v8] Remove Promises API and dependency specifications * Remove other header --- GoogleUtilities.podspec | 1 - .../GULURLSessionDataResponse.h | 31 ------ .../NSURLSession+GULPromises.h | 37 ------- .../GULURLSessionDataResponse.m | 30 ----- .../NSURLSession+GULPromises.m | 46 -------- .../NSURLSession+GULPromisesTests.m | 104 ------------------ Package.swift | 1 - 7 files changed, 250 deletions(-) delete mode 100644 GoogleUtilities/Environment/Public/GoogleUtilities/GULURLSessionDataResponse.h delete mode 100644 GoogleUtilities/Environment/Public/GoogleUtilities/NSURLSession+GULPromises.h delete mode 100644 GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.m delete mode 100644 GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.m delete mode 100644 GoogleUtilities/Tests/Unit/Environment/NSURLSession+GULPromisesTests.m diff --git a/GoogleUtilities.podspec b/GoogleUtilities.podspec index 31387bca..7cf96549 100644 --- a/GoogleUtilities.podspec +++ b/GoogleUtilities.podspec @@ -47,7 +47,6 @@ other Google CocoaPods. They're not intended for direct public usage. 'third_party/IsAppEncrypted/**/*.[mh]' ] es.public_header_files = 'GoogleUtilities/Environment/Public/GoogleUtilities/*.h' - es.dependency 'PromisesObjC', '>= 1.2', '< 3.0' es.dependency 'GoogleUtilities/Privacy' es.frameworks = [ 'Security' diff --git a/GoogleUtilities/Environment/Public/GoogleUtilities/GULURLSessionDataResponse.h b/GoogleUtilities/Environment/Public/GoogleUtilities/GULURLSessionDataResponse.h deleted file mode 100644 index e88eb67b..00000000 --- a/GoogleUtilities/Environment/Public/GoogleUtilities/GULURLSessionDataResponse.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 - -/** The class represents HTTP response received from `NSURLSession`. */ -@interface GULURLSessionDataResponse : NSObject - -@property(nonatomic, readonly) NSHTTPURLResponse *HTTPResponse; -@property(nonatomic, nullable, readonly) NSData *HTTPBody; - -- (instancetype)initWithResponse:(NSHTTPURLResponse *)response HTTPBody:(nullable NSData *)body; - -@end - -NS_ASSUME_NONNULL_END diff --git a/GoogleUtilities/Environment/Public/GoogleUtilities/NSURLSession+GULPromises.h b/GoogleUtilities/Environment/Public/GoogleUtilities/NSURLSession+GULPromises.h deleted file mode 100644 index 7bed005e..00000000 --- a/GoogleUtilities/Environment/Public/GoogleUtilities/NSURLSession+GULPromises.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 - -@class FBLPromise; -@class GULURLSessionDataResponse; - -NS_ASSUME_NONNULL_BEGIN - -/** Promise based API for `NSURLSession`. */ -@interface NSURLSession (GULPromises) - -/** Creates a promise wrapping `-[NSURLSession dataTaskWithRequest:completionHandler:]` method. - * @param URLRequest The request to create a data task with. - * @return A promise that is fulfilled when an HTTP response is received (with any response code), - * or is rejected with the error passed to the task completion. - */ -- (FBLPromise *)gul_dataTaskPromiseWithRequest: - (NSURLRequest *)URLRequest; - -@end - -NS_ASSUME_NONNULL_END diff --git a/GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.m b/GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.m deleted file mode 100644 index 559875a7..00000000 --- a/GoogleUtilities/Environment/URLSessionPromiseWrapper/GULURLSessionDataResponse.m +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 "GoogleUtilities/Environment/Public/GoogleUtilities/GULURLSessionDataResponse.h" - -@implementation GULURLSessionDataResponse - -- (instancetype)initWithResponse:(NSHTTPURLResponse *)response HTTPBody:(NSData *)body { - self = [super init]; - if (self) { - _HTTPResponse = response; - _HTTPBody = body; - } - return self; -} - -@end diff --git a/GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.m b/GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.m deleted file mode 100644 index 6c70310f..00000000 --- a/GoogleUtilities/Environment/URLSessionPromiseWrapper/NSURLSession+GULPromises.m +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 "GoogleUtilities/Environment/Public/GoogleUtilities/NSURLSession+GULPromises.h" - -#if __has_include() -#import -#else -#import "FBLPromises.h" -#endif - -#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULURLSessionDataResponse.h" - -@implementation NSURLSession (GULPromises) - -- (FBLPromise *)gul_dataTaskPromiseWithRequest: - (NSURLRequest *)URLRequest { - return [FBLPromise async:^(FBLPromiseFulfillBlock fulfill, FBLPromiseRejectBlock reject) { - [[self dataTaskWithRequest:URLRequest - completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response, - NSError *_Nullable error) { - if (error) { - reject(error); - } else { - fulfill([[GULURLSessionDataResponse alloc] - initWithResponse:(NSHTTPURLResponse *)response - HTTPBody:data]); - } - }] resume]; - }]; -} - -@end diff --git a/GoogleUtilities/Tests/Unit/Environment/NSURLSession+GULPromisesTests.m b/GoogleUtilities/Tests/Unit/Environment/NSURLSession+GULPromisesTests.m deleted file mode 100644 index 5a4994bc..00000000 --- a/GoogleUtilities/Tests/Unit/Environment/NSURLSession+GULPromisesTests.m +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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 "FBLPromise+Testing.h" -#import "GoogleUtilities/Tests/Unit/Shared/URLSession/FIRURLSessionOCMockStub.h" - -#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULURLSessionDataResponse.h" -#import "GoogleUtilities/Environment/Public/GoogleUtilities/NSURLSession+GULPromises.h" - -#if !TARGET_OS_MACCATALYST - -@interface NSURLSession_GULPromisesTests : XCTestCase -@property(nonatomic) NSURLSession *URLSession; -@property(nonatomic) id URLSessionMock; -@end - -@implementation NSURLSession_GULPromisesTests - -- (void)setUp { - self.URLSession = [NSURLSession - sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]]; - self.URLSessionMock = OCMPartialMock(self.URLSession); -} - -- (void)tearDown { - [self.URLSessionMock stopMocking]; - self.URLSessionMock = nil; - self.URLSession = nil; -} - -- (void)testDataTaskPromiseWithRequestSuccess { - NSURL *url = [NSURL URLWithString:@"https://localhost"]; - NSURLRequest *request = [NSURLRequest requestWithURL:url]; - - NSHTTPURLResponse *expectedResponse = [[NSHTTPURLResponse alloc] initWithURL:url - statusCode:200 - HTTPVersion:@"1.1" - headerFields:nil]; - NSData *expectedBody = [@"body" dataUsingEncoding:NSUTF8StringEncoding]; - - [FIRURLSessionOCMockStub - stubURLSessionDataTaskWithResponse:expectedResponse - body:expectedBody - error:nil - URLSessionMock:self.URLSessionMock - requestValidationBlock:^BOOL(NSURLRequest *_Nonnull sentRequest) { - return [sentRequest isEqual:request]; - }]; - - __auto_type taskPromise = [self.URLSessionMock gul_dataTaskPromiseWithRequest:request]; - - XCTAssert(FBLWaitForPromisesWithTimeout(1.0)); - - XCTAssertTrue(taskPromise.isFulfilled); - XCTAssertNil(taskPromise.error); - XCTAssertEqualObjects(taskPromise.value.HTTPResponse, expectedResponse); - XCTAssertEqualObjects(taskPromise.value.HTTPBody, expectedBody); -} - -- (void)testDataTaskPromiseWithRequestError { - NSURL *url = [NSURL URLWithString:@"https://localhost"]; - NSURLRequest *request = [NSURLRequest requestWithURL:url]; - - NSError *expectedError = [NSError errorWithDomain:@"testDataTaskPromiseWithRequestError" - code:-1 - userInfo:nil]; - - [FIRURLSessionOCMockStub - stubURLSessionDataTaskWithResponse:nil - body:nil - error:expectedError - URLSessionMock:self.URLSessionMock - requestValidationBlock:^BOOL(NSURLRequest *_Nonnull sentRequest) { - return [sentRequest isEqual:request]; - }]; - - __auto_type taskPromise = [self.URLSessionMock gul_dataTaskPromiseWithRequest:request]; - - XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); - - XCTAssertTrue(taskPromise.isRejected); - XCTAssertEqualObjects(taskPromise.error, expectedError); - XCTAssertNil(taskPromise.value); -} - -@end - -#endif // !TARGET_OS_MACCATALYST diff --git a/Package.swift b/Package.swift index 85868c39..729a15ff 100644 --- a/Package.swift +++ b/Package.swift @@ -88,7 +88,6 @@ let package = Package( .target( name: "GoogleUtilities-Environment", dependencies: [ - .product(name: "FBLPromises", package: "Promises"), "third-party-IsAppEncrypted", ], path: "GoogleUtilities/Environment", From 376e409c37548afafd5d335805847db71412e304 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 11 Jun 2024 12:09:20 -0400 Subject: [PATCH 6/7] [v8] Add changelog for removal of Promises dependency --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6364a588..5f348c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,15 @@ | tvOS | **13.0** | | macOS | **10.15** | | watchOS | 7.0 | +- Remove dependency on `FBLPromises`. The following public API have + been removed: + - `- [NSURLSession gul_dataTaskPromiseWithRequest:]` + - `GULURLSessionDataResponse` + The following promise-based public API have been replaced with + completion handler-based alternatives. + - `- [GULKeychainStorage getObjectForKey:objectClass:accessGroup:]` + - `- [GULKeychainStorage setObject:forKey:accessGroup:]` + - `- [GULKeychainStorage removeObjectForKey:accessGroup:]` # 7.13.3 - Rename parameter placeholder in `GULSecureCoding` unarchiving API to avoid From b5c37c565f76d2a98d27be682536658b509e40d8 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 11 Jun 2024 17:02:43 -0400 Subject: [PATCH 7/7] [v8] Remove extraneous parameter from removeObject API (#188) --- .../Public/GoogleUtilities/GULKeychainStorage.h | 7 ++----- .../Environment/SecureStorage/GULKeychainStorage.m | 11 +++++------ .../Tests/Unit/Environment/GULKeychainStorageTests.m | 2 +- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h b/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h index 92870590..eb90ea34 100644 --- a/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h +++ b/GoogleUtilities/Environment/Public/GoogleUtilities/GULKeychainStorage.h @@ -68,13 +68,10 @@ NS_ASSUME_NONNULL_BEGIN /// @param accessGroup The Keychain Access Group. /// @param completionHandler The completion handler to call when the /// synchronized keychain removal is complete. An error is passed to the -/// completion handler if the keychain removal fails. Else, `YES` if the item -/// was removed successfully or doesn’t exist, `NO` otherwise. -/// @note In the event an error occurs, the completion handler will return an -/// error and a boolean to indicate the removal failed (`NO`). +/// completion handler if the keychain removal fails. - (void)removeObjectForKey:(NSString *)key accessGroup:(nullable NSString *)accessGroup - completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler; + completionHandler:(void (^)(NSError *_Nullable error))completionHandler; #if TARGET_OS_OSX /// If not `nil`, then only this keychain will be used to save and read data (see diff --git a/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m b/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m index 0ddb79ea..e6aa69a7 100644 --- a/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m +++ b/GoogleUtilities/Environment/SecureStorage/GULKeychainStorage.m @@ -103,19 +103,18 @@ - (void)setObject:(id)object - (void)removeObjectForKey:(NSString *)key accessGroup:(nullable NSString *)accessGroup - completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler { + completionHandler:(void (^)(NSError *_Nullable error))completionHandler { dispatch_async(self.inMemoryCacheQueue, ^{ [self.inMemoryCache removeObjectForKey:key]; dispatch_async(self.keychainQueue, ^{ NSDictionary *query = [self keychainQueryWithKey:key accessGroup:accessGroup]; NSError *error; - BOOL success = [GULKeychainUtils removeItemWithQuery:query error:&error]; - if (!success) { - completionHandler(NO, error); - return; + if (![GULKeychainUtils removeItemWithQuery:query error:&error]) { + completionHandler(error); + } else { + completionHandler(nil); } - completionHandler(success, nil); }); }); } diff --git a/GoogleUtilities/Tests/Unit/Environment/GULKeychainStorageTests.m b/GoogleUtilities/Tests/Unit/Environment/GULKeychainStorageTests.m index af37dbde..12160c6b 100644 --- a/GoogleUtilities/Tests/Unit/Environment/GULKeychainStorageTests.m +++ b/GoogleUtilities/Tests/Unit/Environment/GULKeychainStorageTests.m @@ -234,7 +234,7 @@ - (void)assertRemoveObjectForKey:(NSString *)key { XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; [self.storage removeObjectForKey:key accessGroup:nil - completionHandler:^(BOOL success, NSError *_Nullable error) { + completionHandler:^(NSError *_Nullable error) { XCTAssertNil(error); [expectation fulfill]; }];