diff options
Diffstat (limited to 'Example/Auth/Tests')
52 files changed, 13714 insertions, 0 deletions
diff --git a/Example/Auth/Tests/FIRAdditionalUserInfoTests.m b/Example/Auth/Tests/FIRAdditionalUserInfoTests.m new file mode 100644 index 0000000..d50380e --- /dev/null +++ b/Example/Auth/Tests/FIRAdditionalUserInfoTests.m @@ -0,0 +1,124 @@ +/* + * Copyright 2017 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 "FIRAdditionalUserInfo_Internal.h" +#import "FIRVerifyAssertionResponse.h" +#import <OCMock/OCMock.h> + +NS_ASSUME_NONNULL_BEGIN + +/** @var kUserName + @brief The fake user name. + */ +static NSString *const kUserName = @"User Doe"; + +/** @var kIsNewUser + @brief The fake flag that indicates the user has signed in for the first time. + */ +static BOOL kIsNewUser = YES; + +/** @var kProviderID + @brief The fake Provider ID. + */ +static NSString *const kProviderID = @"PROVIDER_ID"; + +/** @class FIRAdditionalUserInfoTests + @brief Tests for @c FIRAdditionalUserInfo . + */ +@interface FIRAdditionalUserInfoTests : XCTestCase +@end + +@implementation FIRAdditionalUserInfoTests + +/** @fn googleProfile + @brief The fake user profile under additional user data in @c FIRVerifyAssertionResponse. + */ ++ (NSDictionary *)profile { + static NSDictionary *kProfile = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + kProfile = @{ + @"email": @"user@mail.com", + @"given_name": @"User", + @"family_name": @"Doe" + }; + }); + return kProfile; +} + +/** @fn testAditionalUserInfoCreation + @brief Tests succuessful creation of @c FIRAdditionalUserInfo with + @c initWithProviderID:profile:username: call. + */ +- (void)testAditionalUserInfoCreation { + FIRAdditionalUserInfo *userInfo = + [[FIRAdditionalUserInfo alloc] initWithProviderID:kProviderID + profile:[[self class] profile] + username:kUserName + isNewUser:kIsNewUser]; + XCTAssertEqualObjects(userInfo.providerID, kProviderID); + XCTAssertEqualObjects(userInfo.profile, [[self class] profile]); + XCTAssertEqualObjects(userInfo.username, kUserName); + XCTAssertEqual(userInfo.isNewUser, kIsNewUser); +} + +/** @fn testAditionalUserInfoCreationWithStaticInitializer + @brief Tests succuessful creation of @c FIRAdditionalUserInfo with + @c userInfoWithVerifyAssertionResponse call. + */ +- (void)testAditionalUserInfoCreationWithStaticInitializer { + id mockVeriyAssertionResponse = OCMClassMock([FIRVerifyAssertionResponse class]); + OCMExpect([mockVeriyAssertionResponse providerID]).andReturn(kProviderID); + OCMExpect([mockVeriyAssertionResponse profile]).andReturn([[self class] profile]); + OCMExpect([mockVeriyAssertionResponse username]).andReturn(kUserName); + OCMExpect([mockVeriyAssertionResponse isNewUser]).andReturn(kIsNewUser); + + FIRAdditionalUserInfo *userInfo = + [FIRAdditionalUserInfo userInfoWithVerifyAssertionResponse:mockVeriyAssertionResponse]; + XCTAssertEqualObjects(userInfo.providerID, kProviderID); + XCTAssertEqualObjects(userInfo.profile, [[self class] profile]); + XCTAssertEqualObjects(userInfo.username, kUserName); + XCTAssertEqual(userInfo.isNewUser, kIsNewUser); + OCMVerifyAll(mockVeriyAssertionResponse); +} + +/** @fn testAdditionalUserInfoCoding + @brief Tests successful archiving and unarchiving of @c FIRAdditionalUserInfo. + */ +- (void)testAdditionalUserInfoCoding { + FIRAdditionalUserInfo *userInfo = + [[FIRAdditionalUserInfo alloc] initWithProviderID:kProviderID + profile:[[self class] profile] + username:kUserName + isNewUser:kIsNewUser]; + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:userInfo]; + XCTAssertNotNil(data, @"Should not be nil if archiving succeeded."); + XCTAssertNoThrow([NSKeyedUnarchiver unarchiveObjectWithData:data], + @"Unarchiving should not throw and exception."); + FIRAdditionalUserInfo *unarchivedUserInfo = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + XCTAssertTrue([unarchivedUserInfo isKindOfClass:[FIRAdditionalUserInfo class]], + @"Unarchived object must be of kind FIRAdditionalUserInfo class."); + XCTAssertEqualObjects(unarchivedUserInfo.providerID, userInfo.providerID); + XCTAssertEqualObjects(unarchivedUserInfo.profile, userInfo.profile); + XCTAssertEqualObjects(unarchivedUserInfo.username, userInfo.username); + XCTAssertEqual(unarchivedUserInfo.isNewUser, unarchivedUserInfo.isNewUser); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.h b/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.h new file mode 100644 index 0000000..c0e6d13 --- /dev/null +++ b/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.h @@ -0,0 +1,36 @@ +/* + * Copyright 2017 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 "FIRAppInternal.h" + +/** @category FIRApp (FIRAuthUnitTests) + @brief Tests for @c FIRAuth. + */ +@interface FIRApp (FIRAuthUnitTests) + +/** @fn resetAppForAuthUnitTests + @brief Resets the Firebase app for unit tests. + */ ++ (void)resetAppForAuthUnitTests; + +/** @fn appForAuthUnitTestsWithName: + @brief Creates a Firebase app with given name. + @param name The name for the app. + @return A @c FIRApp with the specified name. + */ ++ (FIRApp *)appForAuthUnitTestsWithName:(NSString *)name; + +@end diff --git a/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m b/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m new file mode 100644 index 0000000..aba4136 --- /dev/null +++ b/Example/Auth/Tests/FIRApp+FIRAuthUnitTests.m @@ -0,0 +1,45 @@ +/* + * Copyright 2017 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 "FIRApp+FIRAuthUnitTests.h" + +#import "FIROptionsInternal.h" + +@implementation FIRApp (FIRAuthUnitTests) + +/** @fn appOptions + @brief Gets Firebase app options to be used for tests. + @return A @c FIROptions instance. + */ ++ (FIROptions *)appOptions { + return [[FIROptions alloc] initInternalWithOptionsDictionary:@{ + @"GOOGLE_APP_ID" : @"1:1085102361755:ios:f790a919483d5bdf", + @"API_KEY" : @"FAKE_API_KEY", + @"GCM_SENDER_ID": @"217397612173" + }]; +} + ++ (void)resetAppForAuthUnitTests { + [FIRApp resetApps]; + [FIRApp configureWithOptions:[self appOptions]]; +} + ++ (FIRApp *)appForAuthUnitTestsWithName:(NSString *)name { + return [[FIRApp alloc] initInstanceWithName:name options:[self appOptions]]; +} + + +@end diff --git a/Example/Auth/Tests/FIRAuthAPNSTokenManagerTests.m b/Example/Auth/Tests/FIRAuthAPNSTokenManagerTests.m new file mode 100644 index 0000000..37d95a6 --- /dev/null +++ b/Example/Auth/Tests/FIRAuthAPNSTokenManagerTests.m @@ -0,0 +1,225 @@ +/* + * Copyright 2017 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 "FIRAuthAPNSToken.h" +#import "FIRAuthAPNSTokenManager.h" +#import <OCMock/OCMock.h> + +NS_ASSUME_NONNULL_BEGIN + +/** @var kRegistrationTimeout + @brief The registration timeout used for testing. + */ +static const NSTimeInterval kRegistrationTimeout = .5; + +/** @var kExpectationTimeout + @brief The test expectation timeout. + @remarks This must be considerably greater than @c kVerificationTimeout . + */ +static const NSTimeInterval kExpectationTimeout = 1; + +/** @class FIRAuthLegacyUIApplication + @brief A fake legacy (< iOS 7) UIApplication class. + @remarks A custom class is needed because `respondsToSelector:` itself cannot be mocked. + */ +@interface FIRAuthLegacyUIApplication : NSObject + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (void)registerForRemoteNotificationTypes:(UIRemoteNotificationType)types; +#pragma clang diagnostic pop + +@end +@implementation FIRAuthLegacyUIApplication + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (void)registerForRemoteNotificationTypes:(UIRemoteNotificationType)types { +} +#pragma clang diagnostic pop + +@end + +/** @class FIRAuthAPNSTokenManagerTests + @brief Unit tests for @c FIRAuthAPNSTokenManager . + */ +@interface FIRAuthAPNSTokenManagerTests : XCTestCase +@end +@implementation FIRAuthAPNSTokenManagerTests { + /** @var _mockApplication + @brief The mock application for testing. + */ + id _mockApplication; + + /** @var _manager + @brief The @c FIRAuthAPNSTokenManager instance under tests. + */ + FIRAuthAPNSTokenManager *_manager; + + /** @var _data + @brief One piece of data used for testing. + */ + NSData *_data; + + /** @var _otherData + @brief Another piece of data used for testing. + */ + NSData *_otherData; +} + +- (void)setUp { + _mockApplication = OCMClassMock([UIApplication class]); + _manager = [[FIRAuthAPNSTokenManager alloc] initWithApplication:_mockApplication]; + _data = [@"qwerty" dataUsingEncoding:NSUTF8StringEncoding]; + _otherData = [@"!@#$" dataUsingEncoding:NSUTF8StringEncoding]; +} + +/** @fn testSetToken + @brief Tests setting and getting the `token` property. + */ +- (void)testSetToken { + XCTAssertNil(_manager.token); + _manager.token = [[FIRAuthAPNSToken alloc] initWithData:_data type:FIRAuthAPNSTokenTypeProd]; + XCTAssertEqualObjects(_manager.token.data, _data); + XCTAssertEqual(_manager.token.type, FIRAuthAPNSTokenTypeProd); + _manager.token = nil; + XCTAssertNil(_manager.token); +} + +/** @fn testDetectTokenType + @brief Tests automatic detection of token type. + */ +- (void)testDetectTokenType { + XCTAssertNil(_manager.token); + _manager.token = [[FIRAuthAPNSToken alloc] initWithData:_data type:FIRAuthAPNSTokenTypeUnknown]; + XCTAssertEqualObjects(_manager.token.data, _data); + XCTAssertNotEqual(_manager.token.type, FIRAuthAPNSTokenTypeUnknown); +} + +/** @fn testCallback + @brief Tests callbacks are called. + */ +- (void)testCallback { + // Add first callback, which is yet to be called. + OCMExpect([_mockApplication registerForRemoteNotifications]); + __block BOOL firstCallbackCalled = NO; + [_manager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token) { + XCTAssertEqualObjects(token.data, _data); + XCTAssertEqual(token.type, FIRAuthAPNSTokenTypeSandbox); + firstCallbackCalled = YES; + }]; + XCTAssertFalse(firstCallbackCalled); + + // Add second callback, which is yet to be called either. + __block BOOL secondCallbackCalled = NO; + [_manager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token) { + XCTAssertEqualObjects(token.data, _data); + XCTAssertEqual(token.type, FIRAuthAPNSTokenTypeSandbox); + secondCallbackCalled = YES; + }]; + XCTAssertFalse(secondCallbackCalled); + + // Setting nil token shouldn't trigger either callbacks. + _manager.token = nil; + XCTAssertFalse(firstCallbackCalled); + XCTAssertFalse(secondCallbackCalled); + XCTAssertNil(_manager.token); + + // Setting a real token should trigger both callbacks. + _manager.token = [[FIRAuthAPNSToken alloc] initWithData:_data type:FIRAuthAPNSTokenTypeSandbox]; + XCTAssertTrue(firstCallbackCalled); + XCTAssertTrue(secondCallbackCalled); + XCTAssertEqualObjects(_manager.token.data, _data); + XCTAssertEqual(_manager.token.type, FIRAuthAPNSTokenTypeSandbox); + + // Add third callback, which should be called back immediately. + __block BOOL thirdCallbackCalled = NO; + [_manager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token) { + XCTAssertEqualObjects(token.data, _data); + XCTAssertEqual(token.type, FIRAuthAPNSTokenTypeSandbox); + thirdCallbackCalled = YES; + }]; + XCTAssertTrue(thirdCallbackCalled); + + // Verify the mock in the main thread. + XCTestExpectation *expectation = [self expectationWithDescription:@"verify mock"]; + dispatch_async(dispatch_get_main_queue(), ^{ + OCMVerifyAll(_mockApplication); + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + +/** @fn testTimeout + @brief Tests callbacks can be timed out. + */ +- (void)testTimeout { + // Set up timeout. + XCTAssertGreaterThan(_manager.timeout, 0); + _manager.timeout = kRegistrationTimeout; + + // Add callback to time out. + OCMExpect([_mockApplication registerForRemoteNotifications]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_manager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token) { + XCTAssertNil(token); + [expectation fulfill]; + }]; + + // Time out. + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockApplication); +} + +/** @fn testLegacyRegistration + @brief Tests remote notification registration on legacy systems. + */ +- (void)testLegacyRegistration { + // Use a custom class for `respondsToSelector:` to work. + _mockApplication = OCMClassMock([FIRAuthLegacyUIApplication class]); + _manager = [[FIRAuthAPNSTokenManager alloc] initWithApplication:_mockApplication]; + + // Add callback. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [[[_mockApplication expect] ignoringNonObjectArgs] registerForRemoteNotificationTypes:0]; +#pragma clang diagnostic pop + __block BOOL callbackCalled = NO; + [_manager getTokenWithCallback:^(FIRAuthAPNSToken *_Nullable token) { + XCTAssertEqualObjects(token.data, _data); + XCTAssertNotEqual(token.type, FIRAuthAPNSTokenTypeUnknown); + callbackCalled = YES; + }]; + XCTAssertFalse(callbackCalled); + + // Set the token. + _manager.token = [[FIRAuthAPNSToken alloc] initWithData:_data type:FIRAuthAPNSTokenTypeUnknown]; + XCTAssertTrue(callbackCalled); + + // Verify the mock in the main thread. + XCTestExpectation *expectation = [self expectationWithDescription:@"verify mock"]; + dispatch_async(dispatch_get_main_queue(), ^{ + OCMVerifyAll(_mockApplication); + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Auth/Tests/FIRAuthAPNSTokenTests.m b/Example/Auth/Tests/FIRAuthAPNSTokenTests.m new file mode 100644 index 0000000..d2cd0b5 --- /dev/null +++ b/Example/Auth/Tests/FIRAuthAPNSTokenTests.m @@ -0,0 +1,43 @@ +/* + * Copyright 2017 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 "FIRAuthAPNSToken.h" + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAuthAPNSTokenTests + @brief Unit tests for @c FIRAuthAPNSToken . + */ +@interface FIRAuthAPNSTokenTests : XCTestCase +@end +@implementation FIRAuthAPNSTokenTests + +/** @fn testInitializer + @brief Tests the initializer of the class. + */ +- (void)testInitializer { + NSData *data = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding]; + FIRAuthAPNSToken *token = [[FIRAuthAPNSToken alloc] initWithData:data + type:FIRAuthAPNSTokenTypeProd]; + XCTAssertEqualObjects(token.data, data); + XCTAssertEqual(token.type, FIRAuthAPNSTokenTypeProd); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Auth/Tests/FIRAuthAppCredentialManagerTests.m b/Example/Auth/Tests/FIRAuthAppCredentialManagerTests.m new file mode 100644 index 0000000..32af8cd --- /dev/null +++ b/Example/Auth/Tests/FIRAuthAppCredentialManagerTests.m @@ -0,0 +1,307 @@ +/* + * Copyright 2017 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 "FIRAuthAppCredential.h" +#import "FIRAuthAppCredentialManager.h" +#import "FIRAuthKeychain.h" +#import <OCMock/OCMock.h> + +#define ANY_ERROR_POINTER ((NSError *__autoreleasing *_Nullable)[OCMArg anyPointer]) +#define SAVE_TO(var) [OCMArg checkWithBlock:^BOOL(id arg) { var = arg; return YES; }] + +/** @var kReceipt + @brief A fake receipt used for testing. + */ +static NSString *const kReceipt = @"FAKE_RECEIPT"; + +/** @var kAnotherReceipt + @brief Another fake receipt used for testing. + */ +static NSString *const kAnotherReceipt = @"OTHER_RECEIPT"; + +/** @var kSecret + @brief A fake secret used for testing. + */ +static NSString *const kSecret = @"FAKE_SECRET"; + +/** @var kAnotherSecret + @brief Another fake secret used for testing. + */ +static NSString *const kAnotherSecret = @"OTHER_SECRET"; + +/** @var kVerificationTimeout + @brief The verification timeout used for testing. + */ +static const NSTimeInterval kVerificationTimeout = 1; + +/** @var kExpectationTimeout + @brief The test expectation timeout. + @remarks This must be considerably greater than @c kVerificationTimeout . + */ +static const NSTimeInterval kExpectationTimeout = 2; + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAuthAppCredentialManagerTests + @brief Unit tests for @c FIRAuthAppCredentialManager . + */ +@interface FIRAuthAppCredentialManagerTests : XCTestCase +@end +@implementation FIRAuthAppCredentialManagerTests { + /** @var _mockKeychain + @brief The mock keychain for testing. + */ + id _mockKeychain; +} + +- (void)setUp { + _mockKeychain = OCMClassMock([FIRAuthKeychain class]); +} + +/** @fn testCompletion + @brief Tests a successfully completed verification flow. + */ +- (void)testCompletion { + // Initial empty state. + OCMExpect([_mockKeychain dataForKey:OCMOCK_ANY error:ANY_ERROR_POINTER]).andReturn(nil); + FIRAuthAppCredentialManager *manager = + [[FIRAuthAppCredentialManager alloc] initWithKeychain:_mockKeychain]; + XCTAssertNil(manager.credential); + OCMVerifyAll(_mockKeychain); + + // Start verification. + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER]) + .andReturn(YES); + [manager didStartVerificationWithReceipt:kReceipt + timeout:kVerificationTimeout + callback:^(FIRAuthAppCredential *credential) { + XCTAssertEqualObjects(credential.receipt, kReceipt); + XCTAssertEqualObjects(credential.secret, kSecret); + [expectation fulfill]; + }]; + XCTAssertNil(manager.credential); + OCMVerifyAll(_mockKeychain); + + // Mismatched receipt shouldn't finish verification. + XCTAssertFalse([manager canFinishVerificationWithReceipt:kAnotherReceipt secret:kAnotherSecret]); + XCTAssertNil(manager.credential); + + // Finish verification. + OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER]) + .andReturn(YES); + XCTAssertTrue([manager canFinishVerificationWithReceipt:kReceipt secret:kSecret]); + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + XCTAssertNotNil(manager.credential); + XCTAssertEqualObjects(manager.credential.receipt, kReceipt); + XCTAssertEqualObjects(manager.credential.secret, kSecret); + OCMVerifyAll(_mockKeychain); + + // Repeated receipt should have no effect. + XCTAssertFalse([manager canFinishVerificationWithReceipt:kReceipt secret:kAnotherSecret]); + XCTAssertEqualObjects(manager.credential.secret, kSecret); +} + +/** @fn testTimeout + @brief Tests a verification flow that times out. + */ +- (void)testTimeout { + // Initial empty state. + OCMExpect([_mockKeychain dataForKey:OCMOCK_ANY error:ANY_ERROR_POINTER]).andReturn(nil); + FIRAuthAppCredentialManager *manager = + [[FIRAuthAppCredentialManager alloc] initWithKeychain:_mockKeychain]; + XCTAssertNil(manager.credential); + OCMVerifyAll(_mockKeychain); + + // Start verification. + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER]) + .andReturn(YES); + [manager didStartVerificationWithReceipt:kReceipt + timeout:kVerificationTimeout + callback:^(FIRAuthAppCredential *credential) { + XCTAssertEqualObjects(credential.receipt, kReceipt); + XCTAssertNil(credential.secret); + [expectation fulfill]; + }]; + XCTAssertNil(manager.credential); + OCMVerifyAll(_mockKeychain); + + // Time-out. + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + XCTAssertNil(manager.credential); + + // Completion after timeout. + OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER]) + .andReturn(YES); + XCTAssertTrue([manager canFinishVerificationWithReceipt:kReceipt secret:kSecret]); + XCTAssertNotNil(manager.credential); + XCTAssertEqualObjects(manager.credential.receipt, kReceipt); + XCTAssertEqualObjects(manager.credential.secret, kSecret); + OCMVerifyAll(_mockKeychain); +} + +/** @fn testMaximumPendingReceipt + @brief Tests the maximum allowed number of pending receipt. + */ +- (void)testMaximumPendingReceipt { + // Initial empty state. + OCMExpect([_mockKeychain dataForKey:OCMOCK_ANY error:ANY_ERROR_POINTER]).andReturn(nil); + FIRAuthAppCredentialManager *manager = + [[FIRAuthAppCredentialManager alloc] initWithKeychain:_mockKeychain]; + XCTAssertNil(manager.credential); + OCMVerifyAll(_mockKeychain); + + // Start verification of the target receipt. + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER]) + .andReturn(YES); + [manager didStartVerificationWithReceipt:kReceipt + timeout:kVerificationTimeout + callback:^(FIRAuthAppCredential *credential) { + XCTAssertEqualObjects(credential.receipt, kReceipt); + XCTAssertEqualObjects(credential.secret, kSecret); + [expectation fulfill]; + }]; + XCTAssertNil(manager.credential); + OCMVerifyAll(_mockKeychain); + + // Start verification of a number of random receipts without overflowing. + for (NSUInteger i = 1; i < manager.maximumNumberOfPendingReceipts; i++) { + OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER]) + .andReturn(YES); + NSString *randomReceipt = [NSString stringWithFormat:@"RANDOM_%lu", (unsigned long)i]; + XCTestExpectation *randomExpectation = [self expectationWithDescription:randomReceipt]; + [manager didStartVerificationWithReceipt:randomReceipt + timeout:kVerificationTimeout + callback:^(FIRAuthAppCredential *credential) { + // They all should get full credential because one is available at this point. + XCTAssertEqualObjects(credential.receipt, kReceipt); + XCTAssertEqualObjects(credential.secret, kSecret); + [randomExpectation fulfill]; + }]; + } + + // Finish verification of target receipt. + OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER]) + .andReturn(YES); + XCTAssertTrue([manager canFinishVerificationWithReceipt:kReceipt secret:kSecret]); + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + XCTAssertNotNil(manager.credential); + XCTAssertEqualObjects(manager.credential.receipt, kReceipt); + XCTAssertEqualObjects(manager.credential.secret, kSecret); + OCMVerifyAll(_mockKeychain); + + // Clear credential to prepare for next round. + [manager clearCredential]; + XCTAssertNil(manager.credential); + + // Start verification of another target receipt. + expectation = [self expectationWithDescription:@"another callback"]; + OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER]) + .andReturn(YES); + [manager didStartVerificationWithReceipt:kAnotherReceipt + timeout:kVerificationTimeout + callback:^(FIRAuthAppCredential *credential) { + XCTAssertEqualObjects(credential.receipt, kAnotherReceipt); + XCTAssertNil(credential.secret); + [expectation fulfill]; + }]; + XCTAssertNil(manager.credential); + OCMVerifyAll(_mockKeychain); + + // Start verification of a number of random receipts to overflow. + for (NSUInteger i = 0; i < manager.maximumNumberOfPendingReceipts; i++) { + OCMExpect([_mockKeychain setData:OCMOCK_ANY forKey:OCMOCK_ANY error:ANY_ERROR_POINTER]) + .andReturn(YES); + NSString *randomReceipt = [NSString stringWithFormat:@"RANDOM_%lu", (unsigned long)i]; + XCTestExpectation *randomExpectation = [self expectationWithDescription:randomReceipt]; + [manager didStartVerificationWithReceipt:randomReceipt + timeout:kVerificationTimeout + callback:^(FIRAuthAppCredential *credential) { + // They all should get partial credential because verification has never completed. + XCTAssertEqualObjects(credential.receipt, randomReceipt); + XCTAssertNil(credential.secret); + [randomExpectation fulfill]; + }]; + } + + // Finish verification of the other target receipt. + XCTAssertFalse([manager canFinishVerificationWithReceipt:kAnotherReceipt secret:kAnotherSecret]); + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + XCTAssertNil(manager.credential); +} + +/** @fn testKeychain + @brief Tests state preservation in the keychain. + */ +- (void)testKeychain { + // Initial empty state. + OCMExpect([_mockKeychain dataForKey:OCMOCK_ANY error:ANY_ERROR_POINTER]).andReturn(nil); + FIRAuthAppCredentialManager *manager = + [[FIRAuthAppCredentialManager alloc] initWithKeychain:_mockKeychain]; + XCTAssertNil(manager.credential); + OCMVerifyAll(_mockKeychain); + + // Start verification. + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + __block NSString *key; + __block NSString *data; + OCMExpect([_mockKeychain setData:SAVE_TO(data) forKey:SAVE_TO(key) error:ANY_ERROR_POINTER]) + .andReturn(YES); + [manager didStartVerificationWithReceipt:kReceipt + timeout:kVerificationTimeout + callback:^(FIRAuthAppCredential *credential) { + XCTAssertEqualObjects(credential.receipt, kReceipt); + XCTAssertNil(credential.secret); + [expectation fulfill]; + }]; + XCTAssertNil(manager.credential); + OCMVerifyAll(_mockKeychain); + + // Time-out. + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + XCTAssertNil(manager.credential); + + // Start a new manager with saved data in keychain. + OCMExpect([_mockKeychain dataForKey:key error:ANY_ERROR_POINTER]).andReturn(data); + manager = [[FIRAuthAppCredentialManager alloc] initWithKeychain:_mockKeychain]; + XCTAssertNil(manager.credential); + OCMVerifyAll(_mockKeychain); + + // Finish verification. + OCMExpect([_mockKeychain setData:SAVE_TO(data) forKey:SAVE_TO(key) error:ANY_ERROR_POINTER]) + .andReturn(YES); + XCTAssertTrue([manager canFinishVerificationWithReceipt:kReceipt secret:kSecret]); + XCTAssertNotNil(manager.credential); + XCTAssertEqualObjects(manager.credential.receipt, kReceipt); + XCTAssertEqualObjects(manager.credential.secret, kSecret); + OCMVerifyAll(_mockKeychain); + + // Start yet another new manager with saved data in keychain. + OCMExpect([_mockKeychain dataForKey:key error:ANY_ERROR_POINTER]).andReturn(data); + manager = [[FIRAuthAppCredentialManager alloc] initWithKeychain:_mockKeychain]; + XCTAssertNotNil(manager.credential); + XCTAssertEqualObjects(manager.credential.receipt, kReceipt); + XCTAssertEqualObjects(manager.credential.secret, kSecret); + OCMVerifyAll(_mockKeychain); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Auth/Tests/FIRAuthAppCredentialTests.m b/Example/Auth/Tests/FIRAuthAppCredentialTests.m new file mode 100644 index 0000000..45dd6ef --- /dev/null +++ b/Example/Auth/Tests/FIRAuthAppCredentialTests.m @@ -0,0 +1,67 @@ +/* + * Copyright 2017 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 "FIRAuthAppCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +/** @var kReceipt + @brief The fake receipt value for testing. + */ +static NSString *const kReceipt = @"RECEIPT"; + +/** @var kSecret + @brief The fake secret value for testing. + */ +static NSString *const kSecret = @"SECRET"; + +/** @class FIRAuthAppCredentialTests + @brief Unit tests for @c FIRAuthAppCredential . + */ +@interface FIRAuthAppCredentialTests : XCTestCase +@end +@implementation FIRAuthAppCredentialTests + +/** @fn testInitializer + @brief Tests the initializer of the class. + */ +- (void)testInitializer { + FIRAuthAppCredential *credential = [[FIRAuthAppCredential alloc] initWithReceipt:kReceipt + secret:kSecret]; + XCTAssertEqualObjects(credential.receipt, kReceipt); + XCTAssertEqualObjects(credential.secret, kSecret); +} + +/** @fn testSecureCoding + @brief Tests the implementation of NSSecureCoding protocol. + */ +- (void)testSecureCoding { + XCTAssertTrue([FIRAuthAppCredential supportsSecureCoding]); + + FIRAuthAppCredential *credential = [[FIRAuthAppCredential alloc] initWithReceipt:kReceipt + secret:kSecret]; + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:credential]; + XCTAssertNotNil(data); + FIRAuthAppCredential *otherCredential = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + XCTAssertEqualObjects(otherCredential.receipt, kReceipt); + XCTAssertEqualObjects(otherCredential.secret, kSecret); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Auth/Tests/FIRAuthAppDelegateProxyTests.m b/Example/Auth/Tests/FIRAuthAppDelegateProxyTests.m new file mode 100644 index 0000000..9ff7473 --- /dev/null +++ b/Example/Auth/Tests/FIRAuthAppDelegateProxyTests.m @@ -0,0 +1,450 @@ +/* + * Copyright 2017 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 <objc/runtime.h> + +#import "FIRAuthAppDelegateProxy.h" +#import <OCMock/OCMock.h> + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRAuthEmptyAppDelegate + @brief A @c UIApplicationDelegate implementation that does nothing. + */ +@interface FIRAuthEmptyAppDelegate : NSObject <UIApplicationDelegate> +@end +@implementation FIRAuthEmptyAppDelegate +@end + +/** @class FIRAuthLegacyAppDelegate + @brief A @c UIApplicationDelegate implementation that implements + `application:didReceiveRemoteNotification:`. + */ +@interface FIRAuthLegacyAppDelegate : NSObject <UIApplicationDelegate> + +/** @var notificationReceived + @brief The last notification received, if any. + */ +@property(nonatomic, copy, nullable) NSDictionary *notificationReceived; + +@end + +@implementation FIRAuthLegacyAppDelegate + +- (void)application:(UIApplication *)application + didReceiveRemoteNotification:(NSDictionary *)userInfo { + self.notificationReceived = userInfo; +} + +@end + +/** @class FIRAuthModernAppDelegate + @brief A @c UIApplicationDelegate implementation that implements both + `application:didRegisterForRemoteNotificationsWithDeviceToken:` and + `application:didReceiveRemoteNotification:fetchCompletionHandler:`. + */ +@interface FIRAuthModernAppDelegate : NSObject <UIApplicationDelegate> + +/** @var deviceTokenReceived + @brief The last device token received, if any. + */ +@property(nonatomic, copy, nullable) NSData *deviceTokenReceived; + +/** @var notificationReceived + @brief The last notification received, if any. + */ +@property(nonatomic, copy, nullable) NSDictionary *notificationReceived; + +@end + +@implementation FIRAuthModernAppDelegate + +- (void)application:(UIApplication *)application + didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { + self.deviceTokenReceived = deviceToken; +} + +- (void)application:(UIApplication *)application + didReceiveRemoteNotification:(NSDictionary *)userInfo + fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { + self.notificationReceived = userInfo; + completionHandler(UIBackgroundFetchResultNewData); +} + +@end + +/** @class FIRAuthAppDelegateProxyTests + @brief Unit tests for @c FIRAuthAppDelegateProxy . + */ +@interface FIRAuthAppDelegateProxyTests : XCTestCase +@end +@implementation FIRAuthAppDelegateProxyTests { + /** @var _mockApplication + @brief The mock UIApplication used for testing. + */ + id _mockApplication; + + /** @var _deviceToken + @brief The fake APNs device token for testing. + */ + NSData *_deviceToken; + + /** @var _notification + @brief The fake notification for testing. + */ + NSDictionary* _notification; +} + +- (void)setUp { + [super setUp]; + _mockApplication = OCMClassMock([UIApplication class]); + _deviceToken = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding]; + _notification = @{ @"zxcv" : @1234 }; +} + +- (void)tearDown { + OCMVerifyAll(_mockApplication); + [super tearDown]; +} + +/** @fn testSharedInstance + @brief Tests that the shared instance is the same one. + */ +- (void)testSharedInstance { + FIRAuthAppDelegateProxy *proxy1 = [FIRAuthAppDelegateProxy sharedInstance]; + FIRAuthAppDelegateProxy *proxy2 = [FIRAuthAppDelegateProxy sharedInstance]; + XCTAssertEqual(proxy1, proxy2); +} + +/** @fn testNilApplication + @brief Tests that initialization fails if the application is nil. + */ +- (void)testNilApplication { + XCTAssertNil([[FIRAuthAppDelegateProxy alloc] initWithApplication:nil]); +} + +/** @fn testNilDelegate + @brief Tests that initialization fails if the application's delegate is nil. + */ +- (void)testNilDelegate { + OCMExpect([_mockApplication delegate]).andReturn(nil); + XCTAssertNil([[FIRAuthAppDelegateProxy alloc] initWithApplication:_mockApplication]); +} + +/** @fn testNonconformingDelegate + @brief Tests that initialization fails if the application's delegate does not conform to + @c UIApplicationDelegate protocol. + */ +- (void)testNonconformingDelegate { + OCMExpect([_mockApplication delegate]).andReturn(@"abc"); + XCTAssertNil([[FIRAuthAppDelegateProxy alloc] initWithApplication:_mockApplication]); +} + +/** @fn testDisabledByBundleEntry + @brief Tests that initialization fails if the proxy is disabled by a bundle entry. + */ +- (void)testDisabledByBundleEntry { + // Swizzle NSBundle's objectForInfoDictionaryKey to return @NO for the specific key. + Method method = class_getInstanceMethod([NSBundle class], @selector(objectForInfoDictionaryKey:)); + __block IMP originalImplementation; + IMP newImplmentation = imp_implementationWithBlock(^id(id object, NSString *key) { + if ([key isEqualToString:@"FirebaseAppDelegateProxyEnabled"]) { + return @NO; + } + typedef id (*Implementation)(id object, SEL cmd, NSString *key); + return ((Implementation)originalImplementation)(object, @selector(objectForInfoDictionaryKey:), + key); + }); + originalImplementation = method_setImplementation(method, newImplmentation); + + // Verify that initialization fails. + FIRAuthEmptyAppDelegate *delegate = [[FIRAuthEmptyAppDelegate alloc] init]; + OCMStub([_mockApplication delegate]).andReturn(delegate); + XCTAssertNil([[FIRAuthAppDelegateProxy alloc] initWithApplication:_mockApplication]); + + // Unswizzle. + imp_removeBlock(method_setImplementation(method, originalImplementation)); +} + +/** @fn testEmptyDelegateOneHandler + @brief Tests that the proxy works against an empty @c UIApplicationDelegate for one handler. + */ +- (void)testEmptyDelegateOneHandler { + FIRAuthEmptyAppDelegate *delegate = [[FIRAuthEmptyAppDelegate alloc] init]; + OCMExpect([_mockApplication delegate]).andReturn(delegate); + __weak id weakProxy; + @autoreleasepool { + FIRAuthAppDelegateProxy *proxy = + [[FIRAuthAppDelegateProxy alloc] initWithApplication:_mockApplication]; + XCTAssertNotNil(proxy); + + // Verify `application:didReceiveRemoteNotification:` is not swizzled. + XCTAssertFalse([delegate respondsToSelector: + @selector(application:didReceiveRemoteNotification:)]); + + // Verify the handler is called after being added. + __weak id weakHandler; + @autoreleasepool { + id mockHandler = OCMProtocolMock(@protocol(FIRAuthAppDelegateHandler)); + [proxy addHandler:mockHandler]; + + // Verify handling of `application:didRegisterForRemoteNotificationsWithDeviceToken:`. + OCMExpect([mockHandler setAPNSToken:_deviceToken]); + [delegate application:_mockApplication + didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken]; + OCMVerifyAll(mockHandler); + + // Verify handling of `application:didReceiveRemoteNotification:fetchCompletionHandler:`. + OCMExpect([mockHandler canHandleNotification:_notification]).andReturn(YES); + __block BOOL fetchCompletionHandlerCalled = NO; + [delegate application:_mockApplication + didReceiveRemoteNotification:_notification + fetchCompletionHandler:^(UIBackgroundFetchResult result) { + XCTAssertEqual(result, UIBackgroundFetchResultNoData); + fetchCompletionHandlerCalled = YES; + }]; + OCMVerifyAll(mockHandler); + XCTAssertTrue(fetchCompletionHandlerCalled); + + weakHandler = mockHandler; + XCTAssertNotNil(weakHandler); + } + // Verify the handler is not retained by the proxy. + XCTAssertNil(weakHandler); + + // Verify nothing bad happens after the handler is released. + [delegate application:_mockApplication + didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken]; + [delegate application:_mockApplication + didReceiveRemoteNotification:_notification + fetchCompletionHandler:^(UIBackgroundFetchResult result) { + XCTFail(@"Should not call completion handler."); + }]; + + weakProxy = proxy; + XCTAssertNotNil(weakProxy); + } + // Verify the proxy does not retain itself. + XCTAssertNil(weakProxy); + // Verify nothing bad happens after the proxy is released. + [delegate application:_mockApplication + didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken]; + [delegate application:_mockApplication + didReceiveRemoteNotification:_notification + fetchCompletionHandler:^(UIBackgroundFetchResult result) { + XCTFail(@"Should not call completion handler."); + }]; +} + +/** @fn testLegacyDelegateTwoHandlers + @brief Tests that the proxy works against a legacy @c UIApplicationDelegate for two handlers. + */ +- (void)testLegacyDelegateTwoHandlers { + FIRAuthLegacyAppDelegate *delegate = [[FIRAuthLegacyAppDelegate alloc] init]; + OCMExpect([_mockApplication delegate]).andReturn(delegate); + __weak id weakProxy; + @autoreleasepool { + FIRAuthAppDelegateProxy *proxy = + [[FIRAuthAppDelegateProxy alloc] initWithApplication:_mockApplication]; + XCTAssertNotNil(proxy); + + // Verify `application:didReceiveRemoteNotification:fetchCompletionHandler` is not swizzled. + XCTAssertFalse([delegate respondsToSelector: + @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)]); + + // Verify the handler is called after being added. + __weak id weakHandler1; + @autoreleasepool { + id mockHandler1 = OCMProtocolMock(@protocol(FIRAuthAppDelegateHandler)); + [proxy addHandler:mockHandler1]; + __weak id weakHandler2; + @autoreleasepool { + id mockHandler2 = OCMProtocolMock(@protocol(FIRAuthAppDelegateHandler)); + [proxy addHandler:mockHandler2]; + + // Verify handling of `application:didRegisterForRemoteNotificationsWithDeviceToken:`. + OCMExpect([mockHandler1 setAPNSToken:_deviceToken]); + OCMExpect([mockHandler2 setAPNSToken:_deviceToken]); + [delegate application:_mockApplication + didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken]; + OCMVerifyAll(mockHandler1); + OCMVerifyAll(mockHandler2); + + // Verify handling of `application:didReceiveRemoteNotification:fetchCompletionHandler:`. + OCMExpect([mockHandler1 canHandleNotification:_notification]).andReturn(YES); + // handler2 shouldn't been invoked because it is already handled by handler1. + [delegate application:_mockApplication didReceiveRemoteNotification:_notification]; + OCMVerifyAll(mockHandler1); + OCMVerifyAll(mockHandler2); + XCTAssertNil(delegate.notificationReceived); + + weakHandler2 = mockHandler2; + XCTAssertNotNil(weakHandler2); + } + // Verify the handler2 is not retained by the proxy. + XCTAssertNil(weakHandler2); + + // Verify handling of `application:didRegisterForRemoteNotificationsWithDeviceToken:`. + OCMExpect([mockHandler1 setAPNSToken:_deviceToken]); + [delegate application:_mockApplication + didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken]; + OCMVerifyAll(mockHandler1); + + // Verify NOT handling of `application:didReceiveRemoteNotification:fetchCompletionHandler:`. + OCMExpect([mockHandler1 canHandleNotification:_notification]).andReturn(NO); + [delegate application:_mockApplication didReceiveRemoteNotification:_notification]; + OCMVerifyAll(mockHandler1); + XCTAssertEqualObjects(delegate.notificationReceived, _notification); + delegate.notificationReceived = nil; + + weakHandler1 = mockHandler1; + XCTAssertNotNil(weakHandler1); + } + // Verify the handler1 is not retained by the proxy. + XCTAssertNil(weakHandler1); + + // Verify the delegate still works after all handlers are released. + [delegate application:_mockApplication + didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken]; + [delegate application:_mockApplication didReceiveRemoteNotification:_notification]; + XCTAssertEqualObjects(delegate.notificationReceived, _notification); + delegate.notificationReceived = nil; + + weakProxy = proxy; + XCTAssertNotNil(weakProxy); + } + // Verify the proxy does not retain itself. + XCTAssertNil(weakProxy); + + // Verify the delegate still works after the proxy is released. + [delegate application:_mockApplication + didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken]; + [delegate application:_mockApplication didReceiveRemoteNotification:_notification]; + XCTAssertEqualObjects(delegate.notificationReceived, _notification); + delegate.notificationReceived = nil; +} + +/** @fn testModernDelegateWithOtherInstance + @brief Tests that the proxy works against a modern @c UIApplicationDelegate along with + another unaffected instance. + */ +- (void)testModernDelegateWithUnaffectedInstance { + FIRAuthModernAppDelegate *delegate = [[FIRAuthModernAppDelegate alloc] init]; + OCMExpect([_mockApplication delegate]).andReturn(delegate); + FIRAuthModernAppDelegate *unaffectedDelegate = [[FIRAuthModernAppDelegate alloc] init]; + __weak id weakProxy; + @autoreleasepool { + FIRAuthAppDelegateProxy *proxy = + [[FIRAuthAppDelegateProxy alloc] initWithApplication:_mockApplication]; + XCTAssertNotNil(proxy); + + // Verify `application:didReceiveRemoteNotification:` is not swizzled. + XCTAssertFalse([delegate respondsToSelector: + @selector(application:didReceiveRemoteNotification:)]); + + // Verify the handler is called after being added. + __weak id weakHandler; + @autoreleasepool { + id mockHandler = OCMProtocolMock(@protocol(FIRAuthAppDelegateHandler)); + [proxy addHandler:mockHandler]; + + // Verify handling of `application:didRegisterForRemoteNotificationsWithDeviceToken:`. + OCMExpect([mockHandler setAPNSToken:_deviceToken]); + [delegate application:_mockApplication + didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken]; + OCMVerifyAll(mockHandler); + XCTAssertEqualObjects(delegate.deviceTokenReceived, _deviceToken); + delegate.deviceTokenReceived = nil; + + // Verify handling of `application:didReceiveRemoteNotification:fetchCompletionHandler:`. + OCMExpect([mockHandler canHandleNotification:_notification]).andReturn(YES); + __block BOOL fetchCompletionHandlerCalled = NO; + [delegate application:_mockApplication + didReceiveRemoteNotification:_notification + fetchCompletionHandler:^(UIBackgroundFetchResult result) { + XCTAssertEqual(result, UIBackgroundFetchResultNoData); + fetchCompletionHandlerCalled = YES; + }]; + OCMVerifyAll(mockHandler); + XCTAssertTrue(fetchCompletionHandlerCalled); + XCTAssertNil(delegate.notificationReceived); + + // Verify unaffected delegate instance. + [unaffectedDelegate application:_mockApplication + didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken]; + XCTAssertEqualObjects(unaffectedDelegate.deviceTokenReceived, _deviceToken); + unaffectedDelegate.deviceTokenReceived = nil; + fetchCompletionHandlerCalled = NO; + [unaffectedDelegate application:_mockApplication + didReceiveRemoteNotification:_notification + fetchCompletionHandler:^(UIBackgroundFetchResult result) { + XCTAssertEqual(result, UIBackgroundFetchResultNewData); + fetchCompletionHandlerCalled = YES; + }]; + XCTAssertTrue(fetchCompletionHandlerCalled); + XCTAssertEqualObjects(unaffectedDelegate.notificationReceived, _notification); + unaffectedDelegate.notificationReceived = nil; + + weakHandler = mockHandler; + XCTAssertNotNil(weakHandler); + } + // Verify the handler is not retained by the proxy. + XCTAssertNil(weakHandler); + + // Verify the delegate still works after the handler is released. + [delegate application:_mockApplication + didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken]; + XCTAssertEqualObjects(delegate.deviceTokenReceived, _deviceToken); + delegate.deviceTokenReceived = nil; + __block BOOL fetchCompletionHandlerCalled = NO; + [delegate application:_mockApplication + didReceiveRemoteNotification:_notification + fetchCompletionHandler:^(UIBackgroundFetchResult result) { + XCTAssertEqual(result, UIBackgroundFetchResultNewData); + fetchCompletionHandlerCalled = YES; + }]; + XCTAssertEqualObjects(delegate.notificationReceived, _notification); + delegate.notificationReceived = nil; + XCTAssertTrue(fetchCompletionHandlerCalled); + + weakProxy = proxy; + XCTAssertNotNil(weakProxy); + } + // Verify the proxy does not retain itself. + XCTAssertNil(weakProxy); + + // Verify the delegate still works after the proxy is released. + [delegate application:_mockApplication + didRegisterForRemoteNotificationsWithDeviceToken:_deviceToken]; + XCTAssertEqualObjects(delegate.deviceTokenReceived, _deviceToken); + delegate.deviceTokenReceived = nil; + __block BOOL fetchCompletionHandlerCalled = NO; + [delegate application:_mockApplication + didReceiveRemoteNotification:_notification + fetchCompletionHandler:^(UIBackgroundFetchResult result) { + XCTAssertEqual(result, UIBackgroundFetchResultNewData); + fetchCompletionHandlerCalled = YES; + }]; + XCTAssertEqualObjects(delegate.notificationReceived, _notification); + delegate.notificationReceived = nil; + XCTAssertTrue(fetchCompletionHandlerCalled); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Auth/Tests/FIRAuthBackendCreateAuthURITests.m b/Example/Auth/Tests/FIRAuthBackendCreateAuthURITests.m new file mode 100644 index 0000000..5d40343 --- /dev/null +++ b/Example/Auth/Tests/FIRAuthBackendCreateAuthURITests.m @@ -0,0 +1,104 @@ +/* + * Copyright 2017 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 "FIRAuthErrorUtils.h" +#import "FIRAuthBackend.h" +#import "FIRCreateAuthURIRequest.h" +#import "FIRCreateAuthURIResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestIdentifier + @brief A test value for @c FIRCreateAuthURIRequest.identifier + */ +static NSString *const kTestIdentifier = @"identifier_value"; + +/** @var kTestContinueURI + @brief A test value for @c FIRCreateAuthURIRequest.continueURI + */ +static NSString *const kTestContinueURI = @"https://www.example.com/"; + +/** @var kTestAPIKey + @brief A test value for @c FIRCreateAuthURIRequest.APIKey + */ +static NSString *const kTestAPIKey = @"apikey_value"; + +/** @var kTestExpectedRequestURL + @brief The URL we are expecting should be requested by valid requests. + */ +static NSString *const kTestExpectedRequestURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/createAuthUri?key=apikey_value"; + +/** @var kTestExpectedKind + @brief The expected value for the "kind" parameter of a successful response. + */ +static NSString *const kTestExpectedKind = @"identitytoolkit#CreateAuthUriResponse"; + +/** @var kTestProviderID1 + @brief A valid value for a provider ID in the @c FIRCreateAuthURIResponse.allProviders array. + */ +static NSString *const kTestProviderID1 = @"google.com"; + +/** @var kTestProviderID2 + @brief A valid value for a provider ID in the @c FIRCreateAuthURIResponse.allProviders array. + */ +static NSString *const kTestProviderID2 = @"facebook.com"; + +/** @class FIRAuthBackendCreateAuthURITests + @brief Unit tests for createAuthURI. + */ +@interface FIRAuthBackendCreateAuthURITests : XCTestCase +@end +@implementation FIRAuthBackendCreateAuthURITests + +- (void)testRequestAndResponseEncoding { + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + + FIRCreateAuthURIRequest *request = + [[FIRCreateAuthURIRequest alloc] initWithIdentifier:kTestIdentifier + continueURI:kTestContinueURI + APIKey:kTestAPIKey]; + + __block FIRCreateAuthURIResponse *createAuthURIResponse; + __block NSError *createAuthURIError; + __block BOOL callbackInvoked; + [FIRAuthBackend createAuthURI:request + callback:^(FIRCreateAuthURIResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + createAuthURIResponse = response; + createAuthURIError = error; + }]; + + XCTAssertEqualObjects(RPCIssuer.requestURL.absoluteString, kTestExpectedRequestURL); + XCTAssertEqualObjects(RPCIssuer.decodedRequest[@"identifier"], kTestIdentifier); + XCTAssertEqualObjects(RPCIssuer.decodedRequest[@"continueUri"], kTestContinueURI); + + [RPCIssuer respondWithJSON:@{ + @"kind" : kTestExpectedKind, + @"allProviders" : @[ kTestProviderID1, kTestProviderID2 ] + }]; + + XCTAssert(callbackInvoked); + XCTAssertNil(createAuthURIError); + XCTAssertEqual(createAuthURIResponse.allProviders.count, 2); + XCTAssertEqualObjects(createAuthURIResponse.allProviders[0], kTestProviderID1); + XCTAssertEqualObjects(createAuthURIResponse.allProviders[1], kTestProviderID2); +} + +@end diff --git a/Example/Auth/Tests/FIRAuthBackendRPCImplementationTests.m b/Example/Auth/Tests/FIRAuthBackendRPCImplementationTests.m new file mode 100644 index 0000000..5930e13 --- /dev/null +++ b/Example/Auth/Tests/FIRAuthBackendRPCImplementationTests.m @@ -0,0 +1,989 @@ +/* + * Copyright 2017 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 "FIRAuthErrorUtils.h" +#import "FIRAuthInternalErrors.h" +#import "FIRAuthBackend.h" +#import "FIRAuthRPCRequest.h" +#import "FIRAuthRPCResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kFakeRequestURL + @brief Used as a fake URL for a fake RPC request. We don't test this here, since it's tested + for the specific RPC requests in their various unit tests. + */ +static NSString *const kFakeRequestURL = @"https://www.google.com/"; + +/** @var kFakeErrorDomain + @brief A value to use for fake @c NSErrors. + */ +static NSString *const kFakeErrorDomain = @"fakeDomain"; + +/** @var kFakeErrorCode + @brief A value to use for fake @c NSErrors. + */ +static const NSUInteger kFakeErrorCode = -1; + +/** @var kUnknownServerErrorMessage + @brief A value to use for fake server errors with an unknown message. + */ +static NSString *const kUnknownServerErrorMessage = @"UNKNOWN_MESSAGE"; + +/** @var kErrorMessageCaptchaRequired + @brief The error message in JSON responses from the server for CAPTCHA required. + */ +static NSString *const kErrorMessageCaptchaRequired = @"CAPTCHA_REQUIRED"; + +/** @var kErrorMessageCaptchaRequiredInvalidPassword + @brief The error message in JSON responses from the server for CAPTCHA required with invalid + password. + */ +static NSString *const kErrorMessageCaptchaRequiredInvalidPassword = + @"CAPTCHA_REQUIRED_INVALID_PASSWORD"; + +/** @var kErrorMessageCaptchaCheckFailed + @brief The error message in JSON responses from the server for CAPTCHA check failed. + */ +static NSString *const kErrorMessageCaptchaCheckFailed = @"CAPTCHA_CHECK_FAILED"; + +/** @var kErrorMessageEmailExists + @brief The error message in JSON responses from the server for user's email already exists. + */ +static NSString *const kErrorMessageEmailExists = @"EMAIL_EXISTS"; + +/** @var kErrorMessageKey + @brief The key for the error message in an error response. + */ +static NSString *const kErrorMessageKey = @"message"; + +/** @var kTestKey + @brief A key to use for a successful response dictionary. + */ +static NSString *const kTestKey = @"TestKey"; + +/** @var kUserDisabledErrorMessage + @brief This is the base error message the server will respond with if the user's account has + been disabled. + */ +static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED"; + +/** @var kFakeUserDisabledCustomErrorMessage + @brief This is a fake custom error message the server can respond with if the user's account has + been disabled. + */ +static NSString *const kFakeUserDisabledCustomErrorMessage = @"The user has been disabled."; + +/** @var kServerErrorDetailMarker + @brief This marker indicates that the server error message contains a detail error message which + should be used instead of the hardcoded client error message. + */ +static NSString *const kServerErrorDetailMarker = @" : "; + +/** @var kTestValue + @brief A value to use for a successful response dictionary. + */ +static NSString *const kTestValue = @"TestValue"; + +/** @class FIRAuthBackendRPCImplementation + @brief Exposes an otherwise private class to these tests. See the real implementation for + documentation. + */ +@interface FIRAuthBackendRPCImplementation : NSObject <FIRAuthBackendImplementation> + +/** @fn postWithRequest:response:callback: + @brief Calls the RPC using HTTP POST. + @remarks Possible error responses: + @see FIRAuthInternalErrorCodeRPCRequestEncodingError + @see FIRAuthInternalErrorCodeJSONSerializationError + @see FIRAuthInternalErrorCodeNetworkError + @see FIRAuthInternalErrorCodeUnexpectedErrorResponse + @see FIRAuthInternalErrorCodeUnexpectedResponse + @see FIRAuthInternalErrorCodeRPCResponseDecodingError + @param request The request. + @param response The empty response to be filled. + @param callback The callback for both success and failure. + */ +- (void)postWithRequest:(id<FIRAuthRPCRequest>)request + response:(id<FIRAuthRPCResponse>)response + callback:(void (^)(NSError *error))callback; + +@end + +/** @extension FIRAuthBackend + @brief This class extension exposes the otherwise private @c implementation method. We use this + here to directly call the @c postWithRequest:response:callback: method of + @c FIRAuthBackendRPCImplementation in some of the tests. + */ +@interface FIRAuthBackend () + +/** @fn implementation + @brief Exposes the otherwise private @c implementation method. We use this here to directly call + the @c postWithRequest:response:callback: method of @c FIRAuthBackendRPCImplementation in + some of the tests. + */ ++ (FIRAuthBackendRPCImplementation *)implementation; + +@end + +/** @class FIRFakeRequest + @brief Allows us to fake a request with deterministic request bodies and encoding errors + returned from the @c FIRAuthRPCRequest-specified @c unencodedHTTPRequestBodyWithError: + method. + */ +@interface FIRFakeRequest : NSObject <FIRAuthRPCRequest> + +/** @fn fakeRequest + @brief A "normal" request which returns an encodable request object with no error. + */ ++ (nullable instancetype)fakeRequest; + +/** @fn fakeRequestWithEncodingError + @brief A request which returns a fake error during the encoding process. + */ ++ (nullable instancetype)fakeRequestWithEncodingError:(NSError *)error; + +/** @fn fakeRequestWithUnserializableRequestBody + @brief A request which returns a request object which can not be properly serialized by + @c NSJSONSerialization. + */ ++ (nullable instancetype)fakeRequestWithUnserializableRequestBody; + +/** @fn fakeRequestWithNoBody + @brief A request which returns a nil request body but no error. + */ ++ (nullable instancetype)fakeRequestWithNoBody; + +/** @fn init + @brief Please use initWithRequestBody:encodingError: + */ +- (nullable instancetype)init NS_UNAVAILABLE; + +/** @fn initWithRequestBody:encodingError: + @brief Designated initializer. + @param requestBody The fake request body to return when @c unencodedHTTPRequestBodyWithError: is + invoked. + @param encodingError The fake error to return when @c unencodedHTTPRequestBodyWithError is + invoked. + */ +- (nullable instancetype)initWithRequestBody:(nullable id)requestBody + encodingError:(nullable NSError *)encodingError + NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRFakeRequest { + /** @var _requestBody + @brief The fake request body object we will return when @c unencodedHTTPRequestBodyWithError: + is invoked. + */ + id _Nullable _requestBody; + + /** @var _requestEncodingError + @brief The fake error object we will return when @c unencodedHTTPRequestBodyWithError: + is invoked. + */ + NSError *_Nullable _requestEncodingError; +} + ++ (nullable instancetype)fakeRequest { + return [[self alloc] initWithRequestBody:@{ } encodingError:nil]; +} + ++ (nullable instancetype)fakeRequestWithEncodingError:(NSError *)error { + return [[self alloc] initWithRequestBody:nil encodingError:error]; +} + ++ (nullable instancetype)fakeRequestWithUnserializableRequestBody { + return [[self alloc] initWithRequestBody:@{ @"unencodableValue" : self } encodingError:nil]; +} + ++ (nullable instancetype)fakeRequestWithNoBody { + return [[self alloc] initWithRequestBody:nil encodingError:nil]; +} + +- (nullable instancetype)initWithRequestBody:(nullable id)requestBody + encodingError:(nullable NSError *)encodingError { + self = [super init]; + if (self) { + _requestBody = requestBody; + _requestEncodingError = encodingError; + } + return self; +} + +- (NSURL *)requestURL { + return [NSURL URLWithString:kFakeRequestURL]; +} + +- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable)error { + if (error) { + *error = _requestEncodingError; + } + return _requestBody; +} + +@end + +/** @class FIRFakeResponse + @brief Allows us to inspect the dictionaries received by @c FIRAuthRPCResponse classes, and + provide deterministic responses to the @c setWithDictionary:error: + methods. + */ +@interface FIRFakeResponse : NSObject <FIRAuthRPCResponse> + +/** @property receivedDictionary + @brief The dictionary passed to the @c setWithDictionary:error: method. + */ +@property(nonatomic, strong, readonly, nullable) NSDictionary *receivedDictionary; + +/** @fn fakeResponse + @brief A "normal" sucessful response (no error, no expected kind.) + */ ++ (nullable instancetype)fakeResponse; + +/** @fn fakeResponseWithDecodingError + @brief A response which returns a fake error during the decoding process. + */ ++ (nullable instancetype)fakeResponseWithDecodingError; + +/** @fn init + @brief Please use initWithDecodingError: + */ +- (nullable instancetype)init NS_UNAVAILABLE; + +- (nullable instancetype)initWithDecodingError:(nullable NSError *)decodingError + NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRFakeResponse { + /** @var _responseDecodingError + @brief The value to return for an error when the @c setWithDictionary:error: method is + invoked. + */ + NSError *_Nullable _responseDecodingError; +} + ++ (nullable instancetype)fakeResponse { + return [[self alloc] initWithDecodingError:nil]; +} + ++ (nullable instancetype)fakeResponseWithDecodingError { + NSError *decodingError = [FIRAuthErrorUtils unexpectedErrorResponseWithDeserializedResponse:self]; + return [[self alloc] initWithDecodingError:decodingError]; +} + +- (nullable instancetype)initWithDecodingError:(nullable NSError *)decodingError { + self = [super init]; + if (self) { + _responseDecodingError = decodingError; + } + return self; +} + +- (BOOL)setWithDictionary:(NSDictionary *)dictionary + error:(NSError *_Nullable *_Nullable)error { + if (_responseDecodingError) { + if (error) { + *error = _responseDecodingError; + } + return NO; + } + _receivedDictionary = dictionary; + return YES; +} + +@end + +/** @class FIRAuthBackendRPCImplementationTests + @brief This set of unit tests is designed primarily to test the possible outcomes of the + @c FIRAuthBackendRPCImplementation.postWithRequest:response:callback: method. + */ +@interface FIRAuthBackendRPCImplementationTests : XCTestCase +@end +@implementation FIRAuthBackendRPCImplementationTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; + + /** @var _RPCImplementation + @brief This backend RPC implementation is used to make fake network requests for each test in + the suite. + */ + FIRAuthBackendRPCImplementation *_RPCImplementation; +} + +- (void)setUp { + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; + _RPCImplementation = [FIRAuthBackend implementation]; +} + +- (void)tearDown { + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + _RPCIssuer = nil; + _RPCImplementation = nil; +} + +/** @fn testRequestEncodingError + @brief This test checks the behaviour of @c postWithRequest:response:callback: when the + request passed returns an error during it's unencodedHTTPRequestBodyWithError: method. + The error returned should be delivered to the caller without any change. + */ +- (void)testRequestEncodingError { + NSError *encodingError = + [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }]; + FIRFakeRequest *request = [FIRFakeRequest fakeRequestWithEncodingError:encodingError]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + // There is no need to call [_RPCIssuer respondWithError:...] in this test because a request + // should never have been tried - and we we know that's the case when we test @c callbackInvoked. + + XCTAssert(callbackInvoked); + + XCTAssertNotNil(callbackError); + XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain); + XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError); + + NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain); + XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeRPCRequestEncodingError); + + NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingUnderlyingError); + XCTAssertEqualObjects(underlyingUnderlyingError.domain, kFakeErrorDomain); + XCTAssertEqual(underlyingUnderlyingError.code, kFakeErrorCode); + + id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]; + XCTAssertNil(deserializedResponse); + + id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey]; + XCTAssertNil(dataResponse); +} + +/** @fn testBodyDataSerializationError + @brief This test checks the behaviour of @c postWithRequest:response:callback: when the + request returns an object which isn't serializable by @c NSJSONSerialization. + The error from @c NSJSONSerialization should be returned as the underlyingError for an + @c NSError with the code @c FIRAuthErrorCodeJSONSerializationError. + */ +- (void)testBodyDataSerializationError { + FIRFakeRequest *request = [FIRFakeRequest fakeRequestWithUnserializableRequestBody]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + // There is no need to call [_RPCIssuer respondWithError:...] in this test because a request + // should never have been tried - and we we know that's the case when we test @c callbackInvoked. + + XCTAssert(callbackInvoked); + + XCTAssertNotNil(callbackError); + XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain); + XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError); + + NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain); + XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeJSONSerializationError); + + NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNil(underlyingUnderlyingError); + + id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]; + XCTAssertNil(deserializedResponse); + + id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey]; + XCTAssertNil(dataResponse); +} + +/** @fn testNetworkError + @brief This test checks to make sure a network error is properly wrapped and forwarded with the + correct code (FIRAuthErrorCodeNetworkError). + */ +- (void)testNetworkError { + FIRFakeRequest *request = [FIRFakeRequest fakeRequest]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + // It shouldn't matter what the error domain/code/userInfo are, any junk values are suitable. The + // implementation should treat any error with no response data as a network error. + NSError *responseError = [NSError errorWithDomain:kFakeErrorDomain + code:kFakeErrorCode + userInfo:nil]; + [_RPCIssuer respondWithError:responseError]; + + XCTAssert(callbackInvoked); + + XCTAssertNotNil(callbackError); + XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain); + XCTAssertEqual(callbackError.code, FIRAuthErrorCodeNetworkError); + + NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertEqualObjects(underlyingError.domain, kFakeErrorDomain); + XCTAssertEqual(underlyingError.code, kFakeErrorCode); + + NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNil(underlyingUnderlyingError); + + id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]; + XCTAssertNil(deserializedResponse); + + id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey]; + XCTAssertNil(dataResponse); +} + +/** @fn testUnparsableErrorResponse + @brief This test checks the behaviour of @c postWithRequest:response:callback: when the + response isn't deserializable by @c NSJSONSerialization and an error + condition (with an associated error response message) was expected. We are expecting to + receive the original network error wrapped in an @c NSError with the code + @c FIRAuthErrorCodeUnexpectedHTTPResponse. + */ +- (void)testUnparsableErrorResponse { + FIRFakeRequest *request = [FIRFakeRequest fakeRequest]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + NSData *data = + [@"<html><body>An error occurred.</body></html>" dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error = + [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }]; + [_RPCIssuer respondWithData:data error:error]; + + XCTAssert(callbackInvoked); + + XCTAssertNotNil(callbackError); + XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain); + XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError); + + NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain); + XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse); + + NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingUnderlyingError); + XCTAssertEqualObjects(underlyingUnderlyingError.domain, kFakeErrorDomain); + XCTAssertEqual(underlyingUnderlyingError.code, kFakeErrorCode); + + id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]; + XCTAssertNil(deserializedResponse); + + id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey]; + XCTAssertNotNil(dataResponse); + XCTAssertEqualObjects(dataResponse, data); +} + +/** @fn testUnparsableSuccessResponse + @brief This test checks the behaviour of @c postWithRequest:response:callback: when the + response isn't deserializable by @c NSJSONSerialization and no error + condition was indicated. We are expecting to + receive the @c NSJSONSerialization error wrapped in an @c NSError with the code + @c FIRAuthErrorCodeUnexpectedServerResponse. + */ +- (void)testUnparsableSuccessResponse { + FIRFakeRequest *request = [FIRFakeRequest fakeRequest]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + NSData *data = + [@"<xml>Some non-JSON value.</xml>" dataUsingEncoding:NSUTF8StringEncoding]; + [_RPCIssuer respondWithData:data error:nil]; + + XCTAssert(callbackInvoked); + + XCTAssertNotNil(callbackError); + XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain); + XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError); + + NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain); + XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedResponse); + + NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingUnderlyingError); + XCTAssertEqualObjects(underlyingUnderlyingError.domain, NSCocoaErrorDomain); + + id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]; + XCTAssertNil(deserializedResponse); + + id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey]; + XCTAssertNotNil(dataResponse); + XCTAssertEqualObjects(dataResponse, data); +} + +/** @fn testNonDictionaryErrorResponse + @brief This test checks the behaviour of @c postWithRequest:response:callback: when the + response deserialized by @c NSJSONSerialization is not a dictionary, and an error was + expected. We are expecting to receive an @c NSError with the code + @c FIRAuthErrorCodeUnexpectedErrorServerResponse with the decoded response in the + @c NSError.userInfo dictionary associated with the key + @c FIRAuthErrorUserInfoDecodedResponseKey. + */ +- (void)testNonDictionaryErrorResponse { + FIRFakeRequest *request = [FIRFakeRequest fakeRequest]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + // We are responding with a JSON-encoded string value representing an array - which is unexpected. + // It should normally be a dictionary, and we need to check for this sort of thing. Because we can + // successfully decode this value, however, we do return it in the error results. We check for + // this array later in the test. + NSData *data = [@"[]" dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error = + [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }]; + [_RPCIssuer respondWithData:data error:error]; + + XCTAssert(callbackInvoked); + + XCTAssertNotNil(callbackError); + XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain); + XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError); + + NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain); + XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse); + + NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNil(underlyingUnderlyingError); + + id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]; + XCTAssertNotNil(deserializedResponse); + XCTAssert([deserializedResponse isKindOfClass:[NSArray class]]); + + id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey]; + XCTAssertNil(dataResponse); +} + +/** @fn testNonDictionarySuccessResponse + @brief This test checks the behaviour of @c postWithRequest:response:callback: when the + response deserialized by @c NSJSONSerialization is not a dictionary, and no error was + expected. We are expecting to receive an @c NSError with the code + @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded response in the + @c NSError.userInfo dictionary associated with the key + @c FIRAuthErrorUserInfoDecodedResponseKey. + */ +- (void)testNonDictionarySuccessResponse { + FIRFakeRequest *request = [FIRFakeRequest fakeRequest]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + // We are responding with a JSON-encoded string value representing an array - which is unexpected. + // It should normally be a dictionary, and we need to check for this sort of thing. Because we can + // successfully decode this value, however, we do return it in the error results. We check for + // this array later in the test. + NSData *data = [@"[]" dataUsingEncoding:NSUTF8StringEncoding]; + [_RPCIssuer respondWithData:data error:nil]; + + XCTAssert(callbackInvoked); + + XCTAssertNotNil(callbackError); + XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain); + XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError); + + NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain); + XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedResponse); + + NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNil(underlyingUnderlyingError); + + id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]; + XCTAssertNotNil(deserializedResponse); + XCTAssert([deserializedResponse isKindOfClass:[NSArray class]]); + + id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey]; + XCTAssertNil(dataResponse); +} + +/** @fn testCaptchaRequiredResponse + @brief This test checks the behaviour of @c postWithRequest:response:callback: when the + we get an error message indicating captcha is required. The backend should not be returning + this error to mobile clients. If it does, we should wrap it in an @c NSError with the code + @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded error message in the + @c NSError.userInfo dictionary associated with the key + @c FIRAuthErrorUserInfoDecodedErrorResponseKey. + */ +- (void)testCaptchaRequiredResponse { + FIRFakeRequest *request = [FIRFakeRequest fakeRequest]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + NSError *error = + [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }]; + [_RPCIssuer respondWithServerErrorMessage:kErrorMessageCaptchaRequired error:error]; + + XCTAssert(callbackInvoked); + + XCTAssertNotNil(callbackError); + XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain); + XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError); + + NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain); + XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse); + + NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNil(underlyingUnderlyingError); + + id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]; + XCTAssertNotNil(deserializedResponse); + XCTAssert([deserializedResponse isKindOfClass:[NSDictionary class]]); + XCTAssertNotNil(deserializedResponse[@"message"]); + + id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey]; + XCTAssertNil(dataResponse); +} + +/** @fn testCaptchaCheckFailedResponse + @brief This test checks the behaviour of @c postWithRequest:response:callback: when the + we get an error message indicating captcha check failed. The backend should not be returning + this error to mobile clients. If it does, we should wrap it in an @c NSError with the code + @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded error message in the + @c NSError.userInfo dictionary associated with the key + @c FIRAuthErrorUserInfoDecodedErrorResponseKey. + */ +- (void)testCaptchaCheckFailedResponse { + FIRFakeRequest *request = [FIRFakeRequest fakeRequest]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + NSError *error = + [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }]; + [_RPCIssuer respondWithServerErrorMessage:kErrorMessageCaptchaCheckFailed error:error]; + + XCTAssert(callbackInvoked); + + XCTAssertNotNil(callbackError); + XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain); + XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError); + + NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain); + XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse); + + NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNil(underlyingUnderlyingError); + + id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]; + XCTAssertNotNil(deserializedResponse); + XCTAssert([deserializedResponse isKindOfClass:[NSDictionary class]]); + XCTAssertNotNil(deserializedResponse[@"message"]); + + id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey]; + XCTAssertNil(dataResponse); +} + +/** @fn testCaptchaRequiredInvalidPasswordResponse + @brief This test checks the behaviour of @c postWithRequest:response:callback: when the + we get an error message indicating captcha is required and an invalid password was entered. + The backend should not be returning this error to mobile clients. If it does, we should wrap + it in an @c NSError with the code + @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded error message in the + @c NSError.userInfo dictionary associated with the key + @c FIRAuthErrorUserInfoDecodedErrorResponseKey. + */ +- (void)testCaptchaRequiredInvalidPasswordResponse { + FIRFakeRequest *request = [FIRFakeRequest fakeRequest]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + NSError *error = + [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }]; + [_RPCIssuer respondWithServerErrorMessage:kErrorMessageCaptchaRequiredInvalidPassword + error:error]; + + XCTAssert(callbackInvoked); + + XCTAssertNotNil(callbackError); + XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain); + XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError); + + NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain); + XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse); + + NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNil(underlyingUnderlyingError); + + id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]; + XCTAssertNotNil(deserializedResponse); + XCTAssert([deserializedResponse isKindOfClass:[NSDictionary class]]); + XCTAssertNotNil(deserializedResponse[@"message"]); + + id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey]; + XCTAssertNil(dataResponse); +} + +/** @fn testDecodableErrorResponseWithUnknownMessage + @brief This test checks the behaviour of @c postWithRequest:response:callback: when the + response deserialized by @c NSJSONSerialization represents a valid error response (and an + error was indicated) but we didn't receive an error message we know about. We are expecting + an @c NSError with the code @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded + error message in the @c NSError.userInfo dictionary associated with the key + @c FIRAuthErrorUserInfoDecodedErrorResponseKey. + */ +- (void)testDecodableErrorResponseWithUnknownMessage { + FIRFakeRequest *request = [FIRFakeRequest fakeRequest]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + // We need to return a valid "error" response here, but we are going to intentionally use a bogus + // error message. + NSError *error = + [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }]; + [_RPCIssuer respondWithServerErrorMessage:kUnknownServerErrorMessage error:error]; + + XCTAssert(callbackInvoked); + + XCTAssertNotNil(callbackError); + XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain); + XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError); + + NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain); + XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse); + + NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNil(underlyingUnderlyingError); + + id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]; + XCTAssertNotNil(deserializedResponse); + XCTAssert([deserializedResponse isKindOfClass:[NSDictionary class]]); + XCTAssertNotNil(deserializedResponse[@"message"]); + + id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey]; + XCTAssertNil(dataResponse); +} + +/** @fn testErrorResponseWithNoErrorMessage + @brief This test checks the behaviour of @c postWithRequest:response:callback: when the + response deserialized by @c NSJSONSerialization is a dictionary, and an error was indicated, + but no error information was present in the decoded response. We are expecting an @c NSError + with the code @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded + response message in the @c NSError.userInfo dictionary associated with the key + @c FIRAuthErrorUserInfoDecodedResponseKey. + */ +- (void)testErrorResponseWithNoErrorMessage { + FIRFakeRequest *request = [FIRFakeRequest fakeRequest]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + NSError *error = + [NSError errorWithDomain:kFakeErrorDomain code:kFakeErrorCode userInfo:@{ }]; + [_RPCIssuer respondWithJSON:@{ } error:error]; + + XCTAssert(callbackInvoked); + + XCTAssertNotNil(callbackError); + XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain); + XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError); + + NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain); + XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeUnexpectedErrorResponse); + + NSError *underlyingUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNil(underlyingUnderlyingError); + + id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]; + XCTAssertNotNil(deserializedResponse); + XCTAssert([deserializedResponse isKindOfClass:[NSDictionary class]]); + + id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey]; + XCTAssertNil(dataResponse); +} + +/** @fn testClientErrorResponse + @brief This test checks the behaviour of @c postWithRequest:response:callback: when the + response contains a client error specified by an error messsage sent from the backend. + */ +- (void)testClientErrorResponse { + FIRFakeRequest *request = [FIRFakeRequest fakeRequest]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackerror; + __block BOOL callBackInvoked; + [_RPCImplementation postWithRequest: request response:response callback:^(NSError *error) { + callBackInvoked = YES; + callbackerror = error; + }]; + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:nil]; + NSString *customErrorMessage =[NSString stringWithFormat:@"%@%@%@", + kUserDisabledErrorMessage, + kServerErrorDetailMarker, + kFakeUserDisabledCustomErrorMessage]; + [_RPCIssuer respondWithServerErrorMessage:customErrorMessage error:error]; + XCTAssertNotNil(callbackerror, @"An error should be returned from callback."); + XCTAssert(callBackInvoked); + XCTAssertEqual(callbackerror.code, FIRAuthErrorCodeUserDisabled); + NSString *customMessage = callbackerror.userInfo[NSLocalizedDescriptionKey]; + XCTAssertEqualObjects(customMessage, kFakeUserDisabledCustomErrorMessage); +} + +/** @fn testUndecodableSuccessResponse + @brief This test checks the behaviour of @c postWithRequest:response:callback: when the + response isn't decodable by the response class but no error condition was expected. We are + expecting to receive an @c NSError with the code + @c FIRAuthErrorCodeUnexpectedServerResponse and the error from @c setWithDictionary:error: + as the value of the underlyingError. + */ +- (void)testUndecodableSuccessResponse { + FIRFakeRequest *request = [FIRFakeRequest fakeRequest]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponseWithDecodingError]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + // It doesn't matter what we respond with here, as long as it's not an error response. The fake + // response will deterministicly simulate a decoding error regardless of the response value it was + // given. + [_RPCIssuer respondWithJSON:@{ }]; + + XCTAssert(callbackInvoked); + + XCTAssertNotNil(callbackError); + XCTAssertEqualObjects(callbackError.domain, FIRAuthErrorDomain); + XCTAssertEqual(callbackError.code, FIRAuthErrorCodeInternalError); + + NSError *underlyingError = callbackError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertEqualObjects(underlyingError.domain, FIRAuthInternalErrorDomain); + XCTAssertEqual(underlyingError.code, FIRAuthInternalErrorCodeRPCResponseDecodingError); + + id deserializedResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]; + XCTAssertNotNil(deserializedResponse); + XCTAssert([deserializedResponse isKindOfClass:[NSDictionary class]]); + + id dataResponse = underlyingError.userInfo[FIRAuthErrorUserInfoDataKey]; + XCTAssertNil(dataResponse); +} + +/** @fn testSuccessfulResponse + @brief Tests that a decoded dictionary is handed to the response instance. + */ +- (void)testSuccessfulResponse { + FIRFakeRequest *request = [FIRFakeRequest fakeRequest]; + FIRFakeResponse *response = [FIRFakeResponse fakeResponse]; + + __block NSError *callbackError; + __block BOOL callbackInvoked; + [_RPCImplementation postWithRequest:request response:response callback:^(NSError *error) { + callbackInvoked = YES; + callbackError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ kTestKey : kTestValue }]; + + XCTAssert(callbackInvoked); + XCTAssertNil(callbackError); + XCTAssertNotNil(response.receivedDictionary); + XCTAssertEqualObjects(response.receivedDictionary[kTestKey], kTestValue); +} + +@end diff --git a/Example/Auth/Tests/FIRAuthDispatcherTests.m b/Example/Auth/Tests/FIRAuthDispatcherTests.m new file mode 100644 index 0000000..9b0abc4 --- /dev/null +++ b/Example/Auth/Tests/FIRAuthDispatcherTests.m @@ -0,0 +1,105 @@ +/* + * Copyright 2017 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 "FIRAuthDispatcher.h" + +/** @var kMaxDifferenceBetweenTimeIntervals + @brief The maximum difference between time intervals (in seconds), after which they will be + considered different. + */ +static const NSTimeInterval kMaxDifferenceBetweenTimeIntervals = 0.1; + +/** @var kTestDelay + @brief Fake time delay before tasks are dispatched. + */ +NSTimeInterval kTestDelay = 0.1; + +/** @var kExpectationTimeout + @brief The maximum time waiting for expectations to fulfill. + */ +static const NSTimeInterval kExpectationTimeout = 1; + +id<OS_dispatch_queue> testWorkQueue; + +/** @class FIRAuthDispatcherTests + @brief Tests for @c FIRAuthDispatcher. + */ +@interface FIRAuthDispatcherTests : XCTestCase +@end +@implementation FIRAuthDispatcherTests + +- (void)setUp { + [super setUp]; + testWorkQueue = dispatch_queue_create("test.work.queue", NULL); +} + +/** @fn testSharedInstance + @brief Tests @c sharedInstance returns the same object. + */ +- (void)testSharedInstance { + FIRAuthDispatcher *instance1 = [FIRAuthDispatcher sharedInstance]; + FIRAuthDispatcher *instance2 = [FIRAuthDispatcher sharedInstance]; + XCTAssertEqual(instance1, instance2); +} + +/** @fn testDispatchAfterDelay + @brief Tests @c dispatchAfterDelay indeed dispatches the specified task after the provided + delay. + */ +- (void)testDispatchAfterDelay { + FIRAuthDispatcher *dispatcher = [FIRAuthDispatcher sharedInstance]; + XCTestExpectation *expectation = [self expectationWithDescription:@"dispatchAfterCallback"]; + NSDate *dateBeforeDispatch = [NSDate date]; + dispatcher.dispatchAfterImplementation = nil; + [dispatcher dispatchAfterDelay:kTestDelay + queue:testWorkQueue + task:^{ + NSTimeInterval timeSinceDispatch = fabs([dateBeforeDispatch timeIntervalSinceNow]) - kTestDelay; + XCTAssert(timeSinceDispatch < kMaxDifferenceBetweenTimeIntervals); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + dispatcher = nil; +} + +/** @fn testSetDispatchAfterImplementation + @brief Tests taht @c dispatchAfterImplementation indeed configures a custom implementation for + @c dispatchAfterDelay. + */ +- (void)testSetDispatchAfterImplementation { + FIRAuthDispatcher *dispatcher = [FIRAuthDispatcher sharedInstance]; + XCTestExpectation *expectation1 = [self expectationWithDescription:@"setDispatchTokenCallback"]; + [dispatcher setDispatchAfterImplementation:^(NSTimeInterval delay, + id<OS_dispatch_queue> _Nonnull queue, + void (^task)(void)) { + XCTAssertEqual(kTestDelay, delay); + XCTAssertEqual(testWorkQueue, queue); + [expectation1 fulfill]; + }]; + [dispatcher dispatchAfterDelay:kTestDelay + queue:testWorkQueue + task:^{ + // Fail to ensure this code is never executed. + XCTFail(); + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + dispatcher.dispatchAfterImplementation = nil;; +} + + +@end diff --git a/Example/Auth/Tests/FIRAuthGlobalWorkQueueTests.m b/Example/Auth/Tests/FIRAuthGlobalWorkQueueTests.m new file mode 100644 index 0000000..a492c3d --- /dev/null +++ b/Example/Auth/Tests/FIRAuthGlobalWorkQueueTests.m @@ -0,0 +1,35 @@ +/* + * Copyright 2017 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 "FIRAuthGlobalWorkQueue.h" + +/** @class FIRAuthGlobalWorkQueueTests + @brief Tests for @c FIRAuthGlobalWorkQueue . + */ +@interface FIRAuthGlobalWorkQueueTests : XCTestCase +@end +@implementation FIRAuthGlobalWorkQueueTests + +- (void)testSingleton { + dispatch_queue_t queue1 = FIRAuthGlobalWorkQueue(); + XCTAssertNotNil(queue1); + dispatch_queue_t queue2 = FIRAuthGlobalWorkQueue(); + XCTAssertEqual(queue1, queue2); +} + +@end diff --git a/Example/Auth/Tests/FIRAuthKeychainTests.m b/Example/Auth/Tests/FIRAuthKeychainTests.m new file mode 100644 index 0000000..374e868 --- /dev/null +++ b/Example/Auth/Tests/FIRAuthKeychainTests.m @@ -0,0 +1,314 @@ +/* + * Copyright 2017 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 <Security/Security.h> +#import <XCTest/XCTest.h> + +#import "FIRAuthKeychain.h" + +/** @var kAccountPrefix + @brief The keychain account prefix assumed by the tests. + */ +static NSString *const kAccountPrefix = @"firebase_auth_1_"; + +/** @var kKey + @brief The key used in tests. + */ +static NSString *const kKey = @"ACCOUNT"; + +/** @var kService + @brief The keychain service used in tests. + */ +static NSString *const kService = @"SERVICE"; + +/** @var kOtherService + @brief Another keychain service used in tests. + */ +static NSString *const kOtherService = @"OTHER_SERVICE"; + +/** @var kData + @brief A piece of keychain data used in tests. + */ +static NSString *const kData = @"DATA"; + +/** @var kOtherData + @brief Another piece of keychain data used in tests. + */ +static NSString *const kOtherData = @"OTHER_DATA"; + +/** @fn accountFromKey + @brief Converts a key string to an account string. + @param key The key string to be converted from. + @return The account string being the conversion result. + */ +static NSString *accountFromKey(NSString *key) { + return [kAccountPrefix stringByAppendingString:key]; +} + +/** @fn dataFromString + @brief Converts a NSString to NSData. + @param string The NSString to be converted from. + @return The NSData being the conversion result. + */ +static NSData *dataFromString(NSString *string) { + return [string dataUsingEncoding:NSUTF8StringEncoding]; +} + +/** @fn stringFromData + @brief Converts a NSData to NSString. + @param data The NSData to be converted from. + @return The NSString being the conversion result. + */ +static NSString *stringFromData(NSData *data) { + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; +} + +/** @fn fakeError + @brief Creates a fake error object. + @return a non-nil NSError instance. + */ +static NSError *fakeError() { + return [NSError errorWithDomain:@"ERROR" code:-1 userInfo:nil]; +} + +/** @class FIRAuthKeychainTests + @brief Tests for @c FIRAuthKeychainTests . + */ +@interface FIRAuthKeychainTests : XCTestCase +@end + +@implementation FIRAuthKeychainTests + +/** @fn testReadNonexisting + @brief Tests reading non-existing keychain item. + */ +- (void)testReadNonexisting { + [self setPassword:nil account:accountFromKey(kKey) service:kService]; + [self setPassword:nil account:kKey service:nil]; // legacy form + FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; + NSError *error = fakeError(); + XCTAssertNil([keychain dataForKey:kKey error:&error]); + XCTAssertNil(error); +} + +/** @fn testReadExisting + @brief Tests reading existing keychain item. + */ +- (void)testReadExisting { + [self setPassword:kData account:accountFromKey(kKey) service:kService]; + FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; + NSError *error = fakeError(); + XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData)); + XCTAssertNil(error); + [self deletePasswordWithAccount:accountFromKey(kKey) service:kService]; +} + +/** @fn testNotReadOtherService + @brief Tests not reading keychain item belonging to other service. + */ +- (void)testNotReadOtherService { + [self setPassword:nil account:accountFromKey(kKey) service:kService]; + [self setPassword:kData account:accountFromKey(kKey) service:kOtherService]; + FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; + NSError *error = fakeError(); + XCTAssertNil([keychain dataForKey:kKey error:&error]); + XCTAssertNil(error); + [self deletePasswordWithAccount:accountFromKey(kKey) service:kOtherService]; +} + +/** @fn testWriteNonexisting + @brief Tests writing new keychain item. + */ +- (void)testWriteNonexisting { + [self setPassword:nil account:accountFromKey(kKey) service:kService]; + FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; + XCTAssertTrue([keychain setData:dataFromString(kData) forKey:kKey error:NULL]); + XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData); + [self deletePasswordWithAccount:accountFromKey(kKey) service:kService]; +} + +/** @fn testWriteExisting + @brief Tests overwriting existing keychain item. + */ +- (void)testWriteExisting { + [self setPassword:kData account:accountFromKey(kKey) service:kService]; + FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; + XCTAssertTrue([keychain setData:dataFromString(kOtherData) forKey:kKey error:NULL]); + XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], + kOtherData); + [self deletePasswordWithAccount:accountFromKey(kKey) service:kService]; +} + +/** @fn testDeleteNonexisting + @brief Tests deleting non-existing keychain item. + */ +- (void)testDeleteNonexisting { + [self setPassword:nil account:accountFromKey(kKey) service:kService]; + FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; + XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]); + XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]); +} + +/** @fn testDeleteExisting + @brief Tests deleting existing keychain item. + */ +- (void)testDeleteExisting { + [self setPassword:kData account:accountFromKey(kKey) service:kService]; + FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; + XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]); + XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]); +} + +/** @fn testReadLegacy + @brief Tests reading legacy keychain item. + */ +- (void)testReadLegacy { + [self setPassword:nil account:accountFromKey(kKey) service:kService]; + [self setPassword:kData account:kKey service:nil]; // legacy form + FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; + NSError *error = fakeError(); + XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData)); + XCTAssertNil(error); + // Legacy item should have been moved to current form. + XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData); + XCTAssertNil([self passwordWithAccount:kKey service:nil]); + [self deletePasswordWithAccount:accountFromKey(kKey) service:kService]; +} + +/** @fn testNotReadLegacy + @brief Tests not reading legacy keychain item because current keychain item exists. + */ +- (void)testNotReadLegacy { + [self setPassword:kData account:accountFromKey(kKey) service:kService]; + [self setPassword:kOtherData account:kKey service:nil]; // legacy form + FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; + NSError *error = fakeError(); + XCTAssertEqualObjects([keychain dataForKey:kKey error:&error], dataFromString(kData)); + XCTAssertNil(error); + // Legacy item should have leave untouched. + XCTAssertEqualObjects([self passwordWithAccount:accountFromKey(kKey) service:kService], kData); + XCTAssertEqualObjects([self passwordWithAccount:kKey service:nil], kOtherData); + [self deletePasswordWithAccount:accountFromKey(kKey) service:kService]; + [self deletePasswordWithAccount:kKey service:nil]; +} + +/** @fn testRemoveLegacy + @brief Tests removing keychain item also removes legacy keychain item. + */ +- (void)testRemoveLegacy { + [self setPassword:kData account:accountFromKey(kKey) service:kService]; + [self setPassword:kOtherData account:kKey service:nil]; // legacy form + FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; + XCTAssertTrue([keychain removeDataForKey:kKey error:NULL]); + XCTAssertNil([self passwordWithAccount:accountFromKey(kKey) service:kService]); + XCTAssertNil([self passwordWithAccount:kKey service:nil]); +} + +/** @fn testNullErrorParameter + @brief Tests that 'NULL' can be safely passed in. + */ +- (void)testNullErrorParameter { + FIRAuthKeychain *keychain = [[FIRAuthKeychain alloc] initWithService:kService]; + [keychain dataForKey:kKey error:NULL]; + [keychain setData:dataFromString(kData) forKey:kKey error:NULL]; + [keychain removeDataForKey:kKey error:NULL]; +} + +#pragma mark - Helpers + +/** @fn passwordWithAccount:service: + @brief Reads a generic password string from the keychain. + @param account The account attribute of the keychain item. + @param service The service attribute of the keychain item, if provided. + @return The generic password string, if the keychain item exists. + */ +- (nullable NSString *)passwordWithAccount:(nonnull NSString *)account + service:(nullable NSString *)service { + NSMutableDictionary *query = [@{ + (__bridge id)kSecReturnData : @YES, + (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrAccount : account, + } mutableCopy]; + if (service) { + query[(__bridge id)kSecAttrService] = service; + } + CFDataRef result; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); + if (status == errSecItemNotFound) { + return nil; + } + XCTAssertEqual(status, errSecSuccess); + return stringFromData((__bridge NSData *)(result)); +} + +/** @fn addPassword:account:service: + @brief Adds a generic password string to the keychain. + @param password The value attribute for the password to write to the keychain item. + @param account The account attribute of the keychain item. + @param service The service attribute of the keychain item, if provided. + */ +- (void)addPassword:(nonnull NSString *)password + account:(nonnull NSString *)account + service:(nullable NSString *)service { + NSMutableDictionary *query = [@{ + (__bridge id)kSecValueData : dataFromString(password), + (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrAccount : account, + } mutableCopy]; + if (service) { + query[(__bridge id)kSecAttrService] = service; + } + OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL); + XCTAssertEqual(status, errSecSuccess); +} + +/** @fn deletePasswordWithAccount:service: + @brief Deletes a generic password string from the keychain. + @param account The account attribute of the keychain item. + @param service The service attribute of the keychain item, if provided. + */ +- (void)deletePasswordWithAccount:(nonnull NSString *)account + service:(nullable NSString *)service { + NSMutableDictionary *query = [@{ + (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrAccount : account, + } mutableCopy]; + if (service) { + query[(__bridge id)kSecAttrService] = service; + } + OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); + XCTAssertEqual(status, errSecSuccess); +} + +/** @fn setPasswordWithString:account:service: + @brief Sets a generic password string to the keychain. + @param password The value attribute of the keychain item, if provided, or nil to delete the + existing password if any. + @param account The account attribute of the keychain item. + @param service The service attribute of the keychain item, if provided. + */ +- (void)setPassword:(nullable NSString *)password + account:(nonnull NSString *)account + service:(nullable NSString *)service { + if ([self passwordWithAccount:account service:service]) { + [self deletePasswordWithAccount:account service:service]; + } + if (password) { + [self addPassword:password account:account service:service]; + } +} + +@end diff --git a/Example/Auth/Tests/FIRAuthNotificationManagerTests.m b/Example/Auth/Tests/FIRAuthNotificationManagerTests.m new file mode 100644 index 0000000..c980eac --- /dev/null +++ b/Example/Auth/Tests/FIRAuthNotificationManagerTests.m @@ -0,0 +1,291 @@ +/* + * Copyright 2017 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 "FIRAuthAppCredential.h" +#import "FIRAuthAppCredentialManager.h" +#import "FIRAuthNotificationManager.h" +#import <OCMock/OCMock.h> + +NS_ASSUME_NONNULL_BEGIN + +/** @var kReceipt + @brief A fake receipt used for testing. + */ +static NSString *const kReceipt = @"FAKE_RECEIPT"; + +/** @var kSecret + @brief A fake secret used for testing. + */ +static NSString *const kSecret = @"FAKE_SECRET"; + +/** @class FIRAuthFakeForwardingDelegate + @brief The base class for a fake UIApplicationDelegate that forwards remote notifications. + */ +@interface FIRAuthFakeForwardingDelegate : NSObject<UIApplicationDelegate> + +/** @property notificationManager + @brief The notification manager to forward. + */ +@property(nonatomic, strong) FIRAuthNotificationManager *notificationManager; + +/** @property forwardsNotification + @brief Whether notifications are being forwarded. + */ +@property(nonatomic, assign) BOOL forwardsNotification; + +/** @property notificationReceived + @brief Whether a notification has been received. + */ +@property(nonatomic, assign) BOOL notificationReceived; + +/** @property notificationhandled + @brief Whether a notification has been handled. + */ +@property(nonatomic, assign) BOOL notificationhandled; + +@end +@implementation FIRAuthFakeForwardingDelegate +@end + +/** @class FIRAuthFakeForwardingDelegate + @brief A fake UIApplicationDelegate that implements the modern deegate method to receive + notification. + */ +@interface FIRAuthModernForwardingDelegate : FIRAuthFakeForwardingDelegate +@end +@implementation FIRAuthModernForwardingDelegate + +- (void)application:(UIApplication *)application + didReceiveRemoteNotification:(NSDictionary *)userInfo + fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { + self.notificationReceived = YES; + if (self.forwardsNotification) { + self.notificationhandled = [self.notificationManager canHandleNotification:userInfo]; + } +} + +@end + +/** @class FIRAuthLegacyForwardingDelegate + @brief A fake UIApplicationDelegate that implements the legacy deegate method to receive + notification. + */ +@interface FIRAuthLegacyForwardingDelegate : FIRAuthFakeForwardingDelegate +@end +@implementation FIRAuthLegacyForwardingDelegate + +- (void)application:(UIApplication *)application + didReceiveRemoteNotification:(NSDictionary *)userInfo { + self.notificationReceived = YES; + if (self.forwardsNotification) { + self.notificationhandled = [self.notificationManager canHandleNotification:userInfo]; + } +} + +@end + +/** @class FIRAuthNotificationManagerTests + @brief Unit tests for @c FIRAuthNotificationManager . + */ +@interface FIRAuthNotificationManagerTests : XCTestCase +@end +@implementation FIRAuthNotificationManagerTests { + /** @var _mockApplication + @brief The mock UIApplication for testing. + */ + id _mockApplication; + + /** @var _mockAppCredentialManager + @brief The mock FIRAuthAppCredentialManager for testing. + */ + id _mockAppCredentialManager; + + /** @var _notificationManager + @brief The FIRAuthNotificationManager to be tested. + */ + FIRAuthNotificationManager *_notificationManager; + + /** @var _modernDelegate + @brief The modern fake UIApplicationDelegate for testing. + */ + FIRAuthModernForwardingDelegate *_modernDelegate; + + /** @var _legacyDelegate + @brief The legacy fake UIApplicationDelegate for testing. + */ + FIRAuthLegacyForwardingDelegate *_legacyDelegate; +} + +- (void)setUp { + _mockApplication = OCMClassMock([UIApplication class]); + _mockAppCredentialManager = OCMClassMock([FIRAuthAppCredentialManager class]); + _notificationManager = + [[FIRAuthNotificationManager alloc] initWithApplication:_mockApplication + appCredentialManager:_mockAppCredentialManager]; + _modernDelegate = [[FIRAuthModernForwardingDelegate alloc] init]; + _modernDelegate.notificationManager = _notificationManager; + _legacyDelegate = [[FIRAuthLegacyForwardingDelegate alloc] init]; + _legacyDelegate.notificationManager = _notificationManager; +} + +/** @fn testForwardingModernDelegate + @brief Tests checking notification forwarding on modern fake delegate. + */ +- (void)testForwardingModernDelegate { + [self verifyForwarding:YES delegate:_modernDelegate]; +} + +/** @fn testForwardingLegacyDelegate + @brief Tests checking notification forwarding on legacy fake delegate. + */ +- (void)testForwardingLegacyDelegate { + [self verifyForwarding:YES delegate:_legacyDelegate]; +} + +/** @fn testNotForwardingModernDelegate + @brief Tests checking notification not forwarding on modern fake delegate. + */ +- (void)testNotForwardingModernDelegate { + [self verifyForwarding:NO delegate:_modernDelegate]; +} + +/** @fn testNotForwardingLegacyDelegate + @brief Tests checking notification not forwarding on legacy fake delegate. + */ +- (void)testNotForwardingLegacyDelegate { + [self verifyForwarding:NO delegate:_legacyDelegate]; +} + +/** @fn verifyForwarding:delegate: + @brief Tests checking notification forwarding on a particular delegate. + @param forwarding Whether the notification is being forwarded or not. + @param delegate The fake UIApplicationDelegate used for testing. + */ +- (void)verifyForwarding:(BOOL)forwarding + delegate:(FIRAuthFakeForwardingDelegate *)delegate { + delegate.forwardsNotification = forwarding; + OCMStub([_mockApplication delegate]).andReturn(delegate); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_notificationManager + checkNotificationForwardingWithCallback:^(BOOL isNotificationBeingForwarded) { + XCTAssertEqual(isNotificationBeingForwarded, forwarding); + [expectation fulfill]; + }]; + XCTAssertFalse(delegate.notificationReceived); + NSTimeInterval timeout = _notificationManager.timeout * (1.5 - forwarding); + [self waitForExpectationsWithTimeout:timeout handler:nil]; + XCTAssertTrue(delegate.notificationReceived); + XCTAssertEqual(delegate.notificationhandled, forwarding); +} + +/** @fn testCachedResult + @brief Test notification forwarding is only checked once. + */ +- (void)testCachedResult { + FIRAuthFakeForwardingDelegate *delegate = _modernDelegate; + [self verifyForwarding:NO delegate:delegate]; + delegate.notificationReceived = NO; + __block BOOL calledBack = NO; + [_notificationManager + checkNotificationForwardingWithCallback:^(BOOL isNotificationBeingForwarded) { + XCTAssertFalse(isNotificationBeingForwarded); + calledBack = YES; + }]; + XCTAssertTrue(calledBack); + XCTAssertFalse(delegate.notificationReceived); +} + +/** @fn testMultipleCallbacks + @brief Test multiple callbacks are handled correctly. + */ +- (void)testMultipleCallbacks { + FIRAuthFakeForwardingDelegate *delegate = _legacyDelegate; + delegate.forwardsNotification = YES; + OCMStub([_mockApplication delegate]).andReturn(delegate); + XCTestExpectation *expectation1 = [self expectationWithDescription:@"callback1"]; + [_notificationManager + checkNotificationForwardingWithCallback:^(BOOL isNotificationBeingForwarded) { + XCTAssertTrue(isNotificationBeingForwarded); + [expectation1 fulfill]; + }]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"callback2"]; + [_notificationManager + checkNotificationForwardingWithCallback:^(BOOL isNotificationBeingForwarded) { + XCTAssertTrue(isNotificationBeingForwarded); + [expectation2 fulfill]; + }]; + XCTAssertFalse(delegate.notificationReceived); + [self waitForExpectationsWithTimeout:_notificationManager.timeout * .5 handler:nil]; + XCTAssertTrue(delegate.notificationReceived); + XCTAssertTrue(delegate.notificationhandled); +} + +/** @fn testPassingToCredentialManager + @brief Test notification with the right structure is passed to credential manager. + */ +- (void)testPassingToCredentialManager { + NSDictionary *payload = @{ @"receipt" : kReceipt, @"secret" : kSecret }; + NSDictionary *notification = @{ @"com.google.firebase.auth" : payload }; + OCMExpect([_mockAppCredentialManager canFinishVerificationWithReceipt:kReceipt secret:kSecret]) + .andReturn(YES); + XCTAssertTrue([_notificationManager canHandleNotification:notification]); + OCMVerifyAll(_mockAppCredentialManager); + + // JSON string form + NSData *data = [NSJSONSerialization dataWithJSONObject:payload options:0 error:NULL]; + NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + notification = @{ @"com.google.firebase.auth" : string }; + OCMExpect([_mockAppCredentialManager canFinishVerificationWithReceipt:kReceipt secret:kSecret]) + .andReturn(YES); + XCTAssertTrue([_notificationManager canHandleNotification:notification]); + OCMVerifyAll(_mockAppCredentialManager); +} + +/** @fn testNotHandling + @brief Test unrecognized notifications are not handled. + */ +- (void)testNotHandling { + XCTAssertFalse([_notificationManager canHandleNotification:@{ + @"random" : @"string" + }]); + XCTAssertFalse([_notificationManager canHandleNotification:@{ + @"com.google.firebase.auth" : @"something wrong" + }]); + XCTAssertFalse([_notificationManager canHandleNotification:@{ + @"com.google.firebase.auth" : @{ + @"receipt" : kReceipt + // missing secret + } + }]); + XCTAssertFalse([_notificationManager canHandleNotification:@{ + @"com.google.firebase.auth" : @{ + // missing receipt + @"secret" : kSecret + } + }]); + XCTAssertFalse([_notificationManager canHandleNotification:@{ + @"com.google.firebase.auth" : @{ + // probing notification does not belong to this instance + @"warning" : @"asdf" + } + }]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Auth/Tests/FIRAuthSerialTaskQueueTests.m b/Example/Auth/Tests/FIRAuthSerialTaskQueueTests.m new file mode 100644 index 0000000..26164d6 --- /dev/null +++ b/Example/Auth/Tests/FIRAuthSerialTaskQueueTests.m @@ -0,0 +1,113 @@ +/* + * Copyright 2017 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 "FIRAuthGlobalWorkQueue.h" +#import "FIRAuthSerialTaskQueue.h" + +/** @var kTimeout + @brief Time-out in seconds waiting for tasks to be executed. + */ +static const NSTimeInterval kTimeout = 1; + +/** @class FIRAuthSerialTaskQueueTests + @brief Tests for @c FIRAuthSerialTaskQueue . + */ +@interface FIRAuthSerialTaskQueueTests : XCTestCase +@end +@implementation FIRAuthSerialTaskQueueTests + +- (void)testExecution { + XCTestExpectation *expectation = [self expectationWithDescription:@"executed"]; + FIRAuthSerialTaskQueue *queue = [[FIRAuthSerialTaskQueue alloc] init]; + [queue enqueueTask:^(FIRAuthSerialTaskCompletionBlock completionArg) { + completionArg(); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testCompletion { + XCTestExpectation *expectation = [self expectationWithDescription:@"executed"]; + FIRAuthSerialTaskQueue *queue = [[FIRAuthSerialTaskQueue alloc] init]; + __block FIRAuthSerialTaskCompletionBlock completion = nil; + [queue enqueueTask:^(FIRAuthSerialTaskCompletionBlock completionArg) { + completion = completionArg; + [expectation fulfill]; + }]; + __block XCTestExpectation *nextExpectation = nil; + __block BOOL executed = NO; + [queue enqueueTask:^(FIRAuthSerialTaskCompletionBlock completionArg) { + executed = YES; + completionArg(); + [nextExpectation fulfill]; + }]; + // The second task should not be executed until the first is completed. + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + XCTAssertNotNil(completion); + XCTAssertFalse(executed); + nextExpectation = [self expectationWithDescription:@"executed next"]; + completion(); + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + XCTAssertTrue(executed); +} + +- (void)testTargetQueue { + XCTestExpectation *expectation = [self expectationWithDescription:@"executed"]; + FIRAuthSerialTaskQueue *queue = [[FIRAuthSerialTaskQueue alloc] init]; + __block BOOL executed = NO; + dispatch_suspend(FIRAuthGlobalWorkQueue()); + [queue enqueueTask:^(FIRAuthSerialTaskCompletionBlock completionArg) { + executed = YES; + completionArg(); + [expectation fulfill]; + }]; + // The task should not executed until the global work queue is resumed. + usleep(kTimeout * USEC_PER_SEC); + XCTAssertFalse(executed); + dispatch_resume(FIRAuthGlobalWorkQueue()); + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testTaskQueueNoAffectTargetQueue { + FIRAuthSerialTaskQueue *queue = [[FIRAuthSerialTaskQueue alloc] init]; + __block FIRAuthSerialTaskCompletionBlock completion = nil; + [queue enqueueTask:^(FIRAuthSerialTaskCompletionBlock completionArg) { + completion = completionArg; + }]; + __block XCTestExpectation *nextExpectation = nil; + __block BOOL executed = NO; + [queue enqueueTask:^(FIRAuthSerialTaskCompletionBlock completionArg) { + executed = YES; + completionArg(); + [nextExpectation fulfill]; + }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"executed"]; + dispatch_async(FIRAuthGlobalWorkQueue(), ^{ + [expectation fulfill]; + }); + // The task queue waiting for completion should not affect the global work queue. + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + XCTAssertNotNil(completion); + XCTAssertFalse(executed); + nextExpectation = [self expectationWithDescription:@"executed next"]; + completion(); + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; + XCTAssertTrue(executed); +} + +@end diff --git a/Example/Auth/Tests/FIRAuthTests.m b/Example/Auth/Tests/FIRAuthTests.m new file mode 100644 index 0000000..3a6f717 --- /dev/null +++ b/Example/Auth/Tests/FIRAuthTests.m @@ -0,0 +1,1743 @@ +/* + * Copyright 2017 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 "FIRAppInternal.h" +#import "EmailPassword/FIREmailAuthProvider.h" +#import "Google/FIRGoogleAuthProvider.h" +#import "Phone/FIRPhoneAuthCredential.h" +#import "Phone/FIRPhoneAuthProvider.h" +#import "FIRAdditionalUserInfo.h" +#import "FIRAuth_Internal.h" +#import "FIRAuthErrorUtils.h" +#import "FIRAuthDispatcher.h" +#import "FIRAuthGlobalWorkQueue.h" +#import "FIRUser_Internal.h" +#import "FIRAuthBackend.h" +#import "FIRCreateAuthURIRequest.h" +#import "FIRCreateAuthURIResponse.h" +#import "FIRGetAccountInfoRequest.h" +#import "FIRGetAccountInfoResponse.h" +#import "FIRGetOOBConfirmationCodeRequest.h" +#import "FIRGetOOBConfirmationCodeResponse.h" +#import "FIRSecureTokenRequest.h" +#import "FIRSecureTokenResponse.h" +#import "FIRResetPasswordRequest.h" +#import "FIRResetPasswordResponse.h" +#import "FIRSetAccountInfoRequest.h" +#import "FIRSetAccountInfoResponse.h" +#import "FIRSignUpNewUserRequest.h" +#import "FIRSignUpNewUserResponse.h" +#import "FIRVerifyCustomTokenRequest.h" +#import "FIRVerifyCustomTokenResponse.h" +#import "FIRVerifyAssertionRequest.h" +#import "FIRVerifyAssertionResponse.h" +#import "FIRVerifyPasswordRequest.h" +#import "FIRVerifyPasswordResponse.h" +#import "FIRVerifyPhoneNumberRequest.h" +#import "FIRVerifyPhoneNumberResponse.h" +#import "FIRApp+FIRAuthUnitTests.h" +#import "OCMStubRecorder+FIRAuthUnitTests.h" +#import <OCMock/OCMock.h> + +/** @var kFirebaseAppName1 + @brief A fake Firebase app name. + */ +static NSString *const kFirebaseAppName1 = @"FIREBASE_APP_NAME_1"; + +/** @var kFirebaseAppName2 + @brief Another fake Firebase app name. + */ +static NSString *const kFirebaseAppName2 = @"FIREBASE_APP_NAME_2"; + +/** @var kAPIKey + @brief The fake API key. + */ +static NSString *const kAPIKey = @"FAKE_API_KEY"; + +/** @var kAccessToken + @brief The fake access token. + */ +static NSString *const kAccessToken = @"ACCESS_TOKEN"; + +/** @var kNewAccessToken + @brief Another fake access token used to simulate token refreshed via automatic token refresh. + */ +NSString *kNewAccessToken = @"NewAccessToken"; + +/** @var kAccessTokenValidInterval + @brief The time to live for the fake access token. + */ +static const NSTimeInterval kAccessTokenTimeToLive = 60 * 60; + +/** @var kTestTokenExpirationTimeInterval + @brief The fake time interval that it takes a token to expire. + */ +static const NSTimeInterval kTestTokenExpirationTimeInterval = 55 * 60; + +/** @var kRefreshToken + @brief The fake refresh token. + */ +static NSString *const kRefreshToken = @"REFRESH_TOKEN"; + +/** @var kEmail + @brief The fake user email. + */ +static NSString *const kEmail = @"user@company.com"; + +/** @var kPassword + @brief The fake user password. + */ +static NSString *const kPassword = @"!@#$%^"; + +/** @var kPasswordHash + @brief The fake user password hash. + */ +static NSString *const kPasswordHash = @"UkVEQUNURUQ="; + +/** @var kLocalID + @brief The fake local user ID. + */ +static NSString *const kLocalID = @"LOCAL_ID"; + +/** @var kDisplayName + @brief The fake user display name. + */ +static NSString *const kDisplayName = @"User Doe"; + +/** @var kGoogleUD + @brief The fake user ID under Google Sign-In. + */ +static NSString *const kGoogleID = @"GOOGLE_ID"; + +/** @var kGoogleEmail + @brief The fake user email under Google Sign-In. + */ +static NSString *const kGoogleEmail = @"user@gmail.com"; + +/** @var kGoogleDisplayName + @brief The fake user display name under Google Sign-In. + */ +static NSString *const kGoogleDisplayName = @"Google Doe"; + +/** @var kGoogleAccessToken + @brief The fake access token from Google Sign-In. + */ +static NSString *const kGoogleAccessToken = @"GOOGLE_ACCESS_TOKEN"; + +/** @var kGoogleIDToken + @brief The fake ID token from Google Sign-In. + */ +static NSString *const kGoogleIDToken = @"GOOGLE_ID_TOKEN"; + +/** @var kCustomToken + @brief The fake custom token to sign in. + */ +static NSString *const kCustomToken = @"CUSTOM_TOKEN"; + +/** @var kVerificationCode + @brief Fake verification code used for testing. + */ +static NSString *const kVerificationCode = @"12345678"; + +/** @var kVerificationID + @brief Fake verification ID for testing. + */ +static NSString *const kVerificationID = @"55432"; + +/** @var kExpectationTimeout + @brief The maximum time waiting for expectations to fulfill. + */ +static const NSTimeInterval kExpectationTimeout = 1; + +/** @var kWaitInterval + @brief The time waiting for background tasks to finish before continue when necessary. + */ +static const NSTimeInterval kWaitInterval = .5; + +/** @class FIRAuthTests + @brief Tests for @c FIRAuth. + */ +@interface FIRAuthTests : XCTestCase +@end +@implementation FIRAuthTests { + + /** @var _mockBackend + @brief The mock @c FIRAuthBackendImplementation . + */ + id _mockBackend; + + /** @var _FIRAuthDispatcherCallback + @brief Used to save a task from FIRAuthDispatcher to be executed later. + */ + __block void (^_Nonnull _FIRAuthDispatcherCallback)(void); +} + +/** @fn googleProfile + @brief The fake user profile under additional user data in @c FIRVerifyAssertionResponse. + */ ++ (NSDictionary *)googleProfile { + static NSDictionary *kGoogleProfile = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + kGoogleProfile = @{ + @"iss": @"https://accounts.google.com\\", + @"email": kGoogleEmail, + @"given_name": @"User", + @"family_name": @"Doe" + }; + }); + return kGoogleProfile; +} + +- (void)setUp { + [super setUp]; + _mockBackend = OCMProtocolMock(@protocol(FIRAuthBackendImplementation)); + [FIRAuthBackend setBackendImplementation:_mockBackend]; + [FIRApp resetAppForAuthUnitTests]; + + // Set FIRAuthDispatcher implementation in order to save the token refresh task for later + // execution. + [[FIRAuthDispatcher sharedInstance] + setDispatchAfterImplementation:^(NSTimeInterval delay, + dispatch_queue_t _Nonnull queue, + void (^task)(void)) { + XCTAssertNotNil(task); + XCTAssert(delay > 0); + XCTAssertEqualObjects(FIRAuthGlobalWorkQueue(), queue); + _FIRAuthDispatcherCallback = task; + }]; +} + +- (void)tearDown { + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [[FIRAuthDispatcher sharedInstance] setDispatchAfterImplementation:nil]; + [super tearDown]; +} + +#pragma mark - Life Cycle Tests + +/** @fn testSingleton + @brief Verifies the @c auth method behaves like a singleton. + */ +- (void)testSingleton { + FIRAuth *auth1 = [FIRAuth auth]; + XCTAssertNotNil(auth1); + FIRAuth *auth2 = [FIRAuth auth]; + XCTAssertEqual(auth1, auth2); +} + +/** @fn testDefaultAuth + @brief Verifies the @c auth method associates with the default Firebase app. + */ +- (void)testDefaultAuth { + FIRAuth *auth1 = [FIRAuth auth]; + FIRAuth *auth2 = [FIRAuth authWithApp:[FIRApp defaultApp]]; + XCTAssertEqual(auth1, auth2); + XCTAssertEqual(auth1.app, [FIRApp defaultApp]); +} + +/** @fn testNilAppException + @brief Verifies the @c auth method raises an exception if the default FIRApp is not configured. + */ +- (void)testNilAppException { + [FIRApp resetApps]; + XCTAssertThrows([FIRAuth auth]); +} + +/** @fn testAppAPIkey + @brief Verifies the API key is correctly copied from @c FIRApp to @c FIRAuth . + */ +- (void)testAppAPIkey { + FIRAuth *auth = [FIRAuth auth]; + XCTAssertEqualObjects(auth.APIKey, kAPIKey); +} + +/** @fn testAppAssociation + @brief Verifies each @c FIRApp instance associates with a @c FIRAuth . + */ +- (void)testAppAssociation { + FIRApp *app1 = [self app1]; + FIRAuth *auth1 = [FIRAuth authWithApp:app1]; + XCTAssertNotNil(auth1); + XCTAssertEqual(auth1.app, app1); + + FIRApp *app2 = [self app2]; + FIRAuth *auth2 = [FIRAuth authWithApp:app2]; + XCTAssertNotNil(auth2); + XCTAssertEqual(auth2.app, app2); + + XCTAssertNotEqual(auth1, auth2); +} + +/** @fn testLifeCycle + @brief Verifies the life cycle of @c FIRAuth is the same as its associated @c FIRApp . + */ +- (void)testLifeCycle { + __weak FIRApp *app; + __weak FIRAuth *auth; + @autoreleasepool { + FIRApp *app1 = [self app1]; + app = app1; + auth = [FIRAuth authWithApp:app1]; + // Verify that neither the app nor the auth is released yet, i.e., the app owns the auth + // because nothing else retains the auth. + XCTAssertNotNil(app); + XCTAssertNotNil(auth); + } + [self waitForTimeIntervel:kWaitInterval]; + // Verify that both the app and the auth are released upon exit of the autorelease pool, + // i.e., the app is the sole owner of the auth. + XCTAssertNil(app); + XCTAssertNil(auth); +} + +/** @fn testGetUID + @brief Verifies that FIRApp's getUIDImplementation is correctly set by FIRAuth. + */ +- (void)testGetUID { + FIRApp *app = [FIRApp defaultApp]; + XCTAssertNotNil(app.getUIDImplementation); + [[FIRAuth auth] signOut:NULL]; + XCTAssertNil(app.getUIDImplementation()); + [self waitForSignIn]; + XCTAssertEqualObjects(app.getUIDImplementation(), kLocalID); +} + +#pragma mark - Server API Tests + +/** @fn testFetchProvidersForEmailSuccess + @brief Tests the flow of a successful @c fetchProvidersForEmail:completion: call. + */ +- (void)testFetchProvidersForEmailSuccess { + NSArray<NSString *> *allProviders = + @[ FIRGoogleAuthProviderID, FIREmailAuthProviderID ]; + OCMExpect([_mockBackend createAuthURI:[OCMArg any] + callback:[OCMArg any]]) + .andCallBlock2(^(FIRCreateAuthURIRequest *_Nullable request, + FIRCreateAuthURIResponseCallback callback) { + XCTAssertEqualObjects(request.identifier, kEmail); + XCTAssertNotNil(request.endpoint); + XCTAssertEqualObjects(request.APIKey, kAPIKey); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockCreateAuthURIResponse = OCMClassMock([FIRCreateAuthURIResponse class]); + OCMStub([mockCreateAuthURIResponse allProviders]).andReturn(allProviders); + callback(mockCreateAuthURIResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] fetchProvidersForEmail:kEmail + completion:^(NSArray<NSString *> *_Nullable providers, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqualObjects(providers, allProviders); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testFetchProvidersForEmailSuccessDeprecatedProviderID + @brief Tests the flow of a successful @c fetchProvidersForEmail:completion: call using the + deprecated FIREmailPasswordAuthProviderID. + */ +- (void)testFetchProvidersForEmailSuccessDeprecatedProviderID { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + NSArray<NSString *> *allProviders = + @[ FIRGoogleAuthProviderID, FIREmailPasswordAuthProviderID ]; +#pragma clang diagnostic pop + OCMExpect([_mockBackend createAuthURI:[OCMArg any] + callback:[OCMArg any]]) + .andCallBlock2(^(FIRCreateAuthURIRequest *_Nullable request, + FIRCreateAuthURIResponseCallback callback) { + XCTAssertEqualObjects(request.identifier, kEmail); + XCTAssertNotNil(request.endpoint); + XCTAssertEqualObjects(request.APIKey, kAPIKey); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockCreateAuthURIResponse = OCMClassMock([FIRCreateAuthURIResponse class]); + OCMStub([mockCreateAuthURIResponse allProviders]).andReturn(allProviders); + callback(mockCreateAuthURIResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] fetchProvidersForEmail:kEmail + completion:^(NSArray<NSString *> *_Nullable providers, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqualObjects(providers, allProviders); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testFetchProvidersForEmailFailure + @brief Tests the flow of a failed @c fetchProvidersForEmail:completion: call. + */ +- (void)testFetchProvidersForEmailFailure { + OCMExpect([_mockBackend createAuthURI:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils tooManyRequestsErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] fetchProvidersForEmail:kEmail + completion:^(NSArray<NSString *> *_Nullable providers, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(providers); + XCTAssertEqual(error.code, FIRAuthErrorCodeTooManyRequests); + XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testPhoneAuthSuccess + @brief Tests the flow of a successful @c signInWithCredential:completion for phone auth. + */ +- (void)testPhoneAuthSuccess { + OCMExpect([_mockBackend verifyPhoneNumber:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyPhoneNumberRequest *_Nullable request, + FIRVerifyPhoneNumberResponseCallback callback) { + XCTAssertEqualObjects(request.verificationCode, kVerificationCode); + XCTAssertEqualObjects(request.verificationID, kVerificationID); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVerifyPhoneResponse = OCMClassMock([FIRVerifyPhoneNumberResponse class]); + [self stubTokensWithMockResponse:mockVerifyPhoneResponse]; + callback(mockVerifyPhoneResponse, nil); + }); + }); + [self expectGetAccountInfo]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *credential = + [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID + verificationCode:kVerificationCode]; + + [[FIRAuth auth] signInWithCredential:credential completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUser:user]; + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUser:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testPhoneAuthMissingVerificationCode + @brief Tests the flow of an unsuccessful @c signInWithCredential:completion for phone auth due + to an empty verification code + */ +- (void)testPhoneAuthMissingVerificationCode { + [self expectGetAccountInfo]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *credential = + [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID + verificationCode:@""]; + + [[FIRAuth auth] signInWithCredential:credential completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(user); + XCTAssertEqual(error.code, FIRAuthErrorCodeMissingVerificationCode); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + +/** @fn testPhoneAuthMissingVerificationID + @brief Tests the flow of an unsuccessful @c signInWithCredential:completion for phone auth due + to an empty verification ID. + */ +- (void)testPhoneAuthMissingVerificationID { + [self expectGetAccountInfo]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *credential = + [[FIRPhoneAuthProvider provider] credentialWithVerificationID:@"" + verificationCode:kVerificationCode]; + + [[FIRAuth auth] signInWithCredential:credential completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(user); + XCTAssertEqual(error.code, FIRAuthErrorCodeMissingVerificationID); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + +/** @fn testSignInWithEmailPasswordSuccess + @brief Tests the flow of a successful @c signInWithEmail:password:completion: call. + */ +- (void)testSignInWithEmailPasswordSuccess { + OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request, + FIRVerifyPasswordResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.email, kEmail); + XCTAssertEqualObjects(request.password, kPassword); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVerifyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]); + [self stubTokensWithMockResponse:mockVerifyPasswordResponse]; + callback(mockVerifyPasswordResponse, nil); + }); + }); + [self expectGetAccountInfo]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] signInWithEmail:kEmail password:kPassword completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUser:user]; + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUser:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInWithEmailPasswordFailure + @brief Tests the flow of a failed @c signInWithEmail:password:completion: call. + */ +- (void)testSignInWithEmailPasswordFailure { + OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils wrongPasswordErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] signInWithEmail:kEmail password:kPassword completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(user); + XCTAssertEqual(error.code, FIRAuthErrorCodeWrongPassword); + XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + XCTAssertNil([FIRAuth auth].currentUser); + OCMVerifyAll(_mockBackend); +} + +/** @fn testResetPasswordSuccess + @brief Tests the flow of a successful @c confirmPasswordResetWithCode:newPassword:completion: + call. + */ +- (void)testResetPasswordSuccess { + NSString *fakeEmail = @"fakeEmail"; + NSString *fakeCode = @"fakeCode"; + NSString *fakeNewPassword = @"fakeNewPassword"; + OCMExpect([_mockBackend resetPassword:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRResetPasswordRequest *_Nullable request, + FIRResetPasswordCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.oobCode, fakeCode); + XCTAssertEqualObjects(request.updatedPassword, fakeNewPassword); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockResetPasswordResponse = OCMClassMock([FIRResetPasswordResponse class]); + OCMStub([mockResetPasswordResponse email]).andReturn(fakeEmail); + callback(mockResetPasswordResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] confirmPasswordResetWithCode:fakeCode + newPassword:fakeNewPassword + completion:^(NSError *_Nullable error) { + + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testResetPasswordFailure + @brief Tests the flow of a failed @c confirmPasswordResetWithCode:newPassword:completion: + call. + */ +- (void)testResetPasswordFailure { + NSString *fakeCode = @"fakeCode"; + NSString *fakeNewPassword = @"fakeNewPassword"; + OCMExpect([_mockBackend resetPassword:[OCMArg any] callback:[OCMArg any]]) + ._andDispatchError2([FIRAuthErrorUtils invalidActionCodeErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] confirmPasswordResetWithCode:fakeCode + newPassword:fakeNewPassword + completion:^(NSError *_Nullable error) { + + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidActionCode); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testCheckActionCodeSuccess + @brief Tests the flow of a successful @c checkActionCode:completion call. + */ +- (void)testCheckActionCodeSuccess { + NSString *verifyEmailRequestType = @"VERIFY_EMAIL"; + NSString *fakeEmail = @"fakeEmail"; + NSString *fakeNewEmail = @"fakeNewEmail"; + NSString *fakeCode = @"fakeCode"; + OCMExpect([_mockBackend resetPassword:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRResetPasswordRequest *_Nullable request, + FIRResetPasswordCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.oobCode, fakeCode); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockResetPasswordResponse = OCMClassMock([FIRResetPasswordResponse class]); + OCMStub([mockResetPasswordResponse email]).andReturn(fakeEmail); + OCMStub([mockResetPasswordResponse verifiedEmail]).andReturn(fakeNewEmail); + OCMStubRecorder *stub = + OCMStub([(FIRResetPasswordResponse *) mockResetPasswordResponse requestType]); + stub.andReturn(verifyEmailRequestType); + callback(mockResetPasswordResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] checkActionCode:fakeCode completion:^(FIRActionCodeInfo *_Nullable info, + NSError *_Nullable error) { + + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(error); + XCTAssertEqual(info.operation, FIRActionCodeOperationVerifyEmail); + XCTAssert([fakeNewEmail isEqualToString:[info dataForKey:FIRActionCodeEmailKey]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testCheckActionCodeFailure + @brief Tests the flow of a failed @c checkActionCode:completion call. + */ +- (void)testCheckActionCodeFailure { + NSString *fakeCode = @"fakeCode"; + OCMExpect([_mockBackend resetPassword:[OCMArg any] callback:[OCMArg any]]) + ._andDispatchError2([FIRAuthErrorUtils expiredActionCodeErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] checkActionCode:fakeCode completion:^(FIRActionCodeInfo *_Nullable info, + NSError *_Nullable error) { + + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, FIRAuthErrorCodeExpiredActionCode); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testApplyActionCodeSuccess + @brief Tests the flow of a successful @c applyActionCode:completion call. + */ +- (void)testApplyActionCodeSuccess { + NSString *fakeCode = @"fakeCode"; + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request, + FIRSetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.OOBCode, fakeCode); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]); + callback(mockSetAccountInfoResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] applyActionCode:fakeCode completion:^(NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testApplyActionCodeFailure + @brief Tests the flow of a failed @c checkActionCode:completion call. + */ +- (void)testApplyActionCodeFailure { + NSString *fakeCode = @"fakeCode"; + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + ._andDispatchError2([FIRAuthErrorUtils invalidActionCodeErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] applyActionCode:fakeCode completion:^(NSError *_Nullable error) { + + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidActionCode); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testVerifyPasswordResetCodeSuccess + @brief Tests the flow of a successful @c verifyPasswordResetCode:completion call. + */ +- (void)testVerifyPasswordResetCodeSuccess { + NSString *passwordResetRequestType = @"PASSWORD_RESET"; + NSString *fakeEmail = @"fakeEmail"; + NSString *fakeCode = @"fakeCode"; + OCMExpect([_mockBackend resetPassword:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRResetPasswordRequest *_Nullable request, + FIRResetPasswordCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.oobCode, fakeCode); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockResetPasswordResponse = OCMClassMock([FIRResetPasswordResponse class]); + OCMStub([mockResetPasswordResponse email]).andReturn(fakeEmail); + OCMStubRecorder *stub = + OCMStub([(FIRResetPasswordResponse *) mockResetPasswordResponse requestType]); + stub.andReturn(passwordResetRequestType); + callback(mockResetPasswordResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] verifyPasswordResetCode:fakeCode completion:^(NSString *_Nullable email, + NSError *_Nullable error) { + + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(error); + XCTAssertEqual(email, fakeEmail); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testVerifyPasswordResetCodeFailure + @brief Tests the flow of a failed @c verifyPasswordResetCode:completion call. + */ +- (void)testVeridyPasswordResetCodeFailure { + NSString *fakeCode = @"fakeCode"; + OCMExpect([_mockBackend resetPassword:[OCMArg any] callback:[OCMArg any]]) + ._andDispatchError2([FIRAuthErrorUtils invalidActionCodeErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] verifyPasswordResetCode:fakeCode completion:^(NSString *_Nullable email, + NSError *_Nullable error) { + + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidActionCode); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInWithEmailCredentialSuccess + @brief Tests the flow of a successfully @c signInWithCredential:completion: call with an + email-password credential. + */ +- (void)testSignInWithEmailCredentialSuccess { + OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request, + FIRVerifyPasswordResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.email, kEmail); + XCTAssertEqualObjects(request.password, kPassword); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVeriyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]); + [self stubTokensWithMockResponse:mockVeriyPasswordResponse]; + callback(mockVeriyPasswordResponse, nil); + }); + }); + [self expectGetAccountInfo]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *emailCredential = + [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword]; + [[FIRAuth auth] signInWithCredential:emailCredential completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUser:user]; + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUser:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInWithEmailCredentialSuccess + @brief Tests the flow of a successfully @c signInWithCredential:completion: call with an + email-password credential using the deprecated FIREmailPasswordAuthProvider. + */ +- (void)testSignInWithEmailCredentialSuccessWithDepricatedProvider { + OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request, + FIRVerifyPasswordResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.email, kEmail); + XCTAssertEqualObjects(request.password, kPassword); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVeriyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]); + [self stubTokensWithMockResponse:mockVeriyPasswordResponse]; + callback(mockVeriyPasswordResponse, nil); + }); + }); + [self expectGetAccountInfo]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + FIRAuthCredential *emailCredential = + [FIREmailPasswordAuthProvider credentialWithEmail:kEmail password:kPassword]; +#pragma clang diagnostic pop + [[FIRAuth auth] signInWithCredential:emailCredential completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUser:user]; + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUser:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInWithEmailCredentialFailure + @brief Tests the flow of a failed @c signInWithCredential:completion: call with an + email-password credential. + */ +- (void)testSignInWithEmailCredentialFailure { + OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils userDisabledErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *emailCredential = + [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword]; + [[FIRAuth auth] signInWithCredential:emailCredential completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(user); + XCTAssertEqual(error.code, FIRAuthErrorCodeUserDisabled); + XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + XCTAssertNil([FIRAuth auth].currentUser); + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInWithEmailCredentialEmptyPassword + @brief Tests the flow of a failed @c signInWithCredential:completion: call with an + email-password credential using an empty password. This error occurs on the client side, + so there is no need to fake an RPC response. + */ +- (void)testSignInWithEmailCredentialEmptyPassword { + NSString *emptyString = @""; + [self expectGetAccountInfo]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *emailCredential = + [FIREmailAuthProvider credentialWithEmail:kEmail password:emptyString]; + [[FIRAuth auth] signInWithCredential:emailCredential completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(error.code, FIRAuthErrorCodeWrongPassword); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + +/** @fn testSignInWithGoogleAccountExistsError + @brief Tests the flow of a failed @c signInWithCredential:completion: with a Google credential + where the backend returns a needs @needConfirmation equal to true. An + FIRAuthErrorCodeAccountExistsWithDifferentCredential error should be thrown. + */ +- (void)testSignInWithGoogleAccountExistsError { + OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request, + FIRVerifyAssertionResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.providerID, FIRGoogleAuthProviderID); + XCTAssertEqualObjects(request.providerIDToken, kGoogleIDToken); + XCTAssertEqualObjects(request.providerAccessToken, kGoogleAccessToken); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVeriyAssertionResponse = OCMClassMock([FIRVerifyAssertionResponse class]); + OCMStub([mockVeriyAssertionResponse needConfirmation]).andReturn(YES); + OCMStub([mockVeriyAssertionResponse email]).andReturn(kEmail); + [self stubTokensWithMockResponse:mockVeriyAssertionResponse]; + callback(mockVeriyAssertionResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *googleCredential = + [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken]; + [[FIRAuth auth] signInWithCredential:googleCredential completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(error.code, FIRAuthErrorCodeAccountExistsWithDifferentCredential); + XCTAssertEqualObjects(error.userInfo[FIRAuthErrorUserInfoEmailKey], kEmail); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInWithGoogleCredentialSuccess + @brief Tests the flow of a successful @c signInWithCredential:completion: call with an + Google Sign-In credential. + */ +- (void)testSignInWithGoogleCredentialSuccess { + OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request, + FIRVerifyAssertionResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.providerID, FIRGoogleAuthProviderID); + XCTAssertEqualObjects(request.providerIDToken, kGoogleIDToken); + XCTAssertEqualObjects(request.providerAccessToken, kGoogleAccessToken); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVeriyAssertionResponse = OCMClassMock([FIRVerifyAssertionResponse class]); + OCMStub([mockVeriyAssertionResponse federatedID]).andReturn(kGoogleID); + OCMStub([mockVeriyAssertionResponse providerID]).andReturn(FIRGoogleAuthProviderID); + OCMStub([mockVeriyAssertionResponse localID]).andReturn(kLocalID); + OCMStub([mockVeriyAssertionResponse displayName]).andReturn(kGoogleDisplayName); + [self stubTokensWithMockResponse:mockVeriyAssertionResponse]; + callback(mockVeriyAssertionResponse, nil); + }); + }); + [self expectGetAccountInfoGoogle]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *googleCredential = + [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken]; + [[FIRAuth auth] signInWithCredential:googleCredential completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserGoogle:user]; + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUserGoogle:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInAndRetrieveDataWithCredentialSuccess + @brief Tests the flow of a successful @c signInAndRetrieveDataWithCredential:completion: call + with an Google Sign-In credential. + */ +- (void)testSignInAndRetrieveDataWithCredentialSuccess { + OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request, + FIRVerifyAssertionResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.providerID, FIRGoogleAuthProviderID); + XCTAssertEqualObjects(request.providerIDToken, kGoogleIDToken); + XCTAssertEqualObjects(request.providerAccessToken, kGoogleAccessToken); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVeriyAssertionResponse = OCMClassMock([FIRVerifyAssertionResponse class]); + OCMStub([mockVeriyAssertionResponse federatedID]).andReturn(kGoogleID); + OCMStub([mockVeriyAssertionResponse providerID]).andReturn(FIRGoogleAuthProviderID); + OCMStub([mockVeriyAssertionResponse localID]).andReturn(kLocalID); + OCMStub([mockVeriyAssertionResponse displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockVeriyAssertionResponse profile]).andReturn([[self class] googleProfile]); + OCMStub([mockVeriyAssertionResponse username]).andReturn(kDisplayName); + [self stubTokensWithMockResponse:mockVeriyAssertionResponse]; + callback(mockVeriyAssertionResponse, nil); + }); + }); + [self expectGetAccountInfoGoogle]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *googleCredential = + [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken]; + [[FIRAuth auth] signInAndRetrieveDataWithCredential:googleCredential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserGoogle:authResult.user]; + XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]); + XCTAssertEqualObjects(authResult.additionalUserInfo.username, kDisplayName); + XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRGoogleAuthProviderID); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUserGoogle:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInWithGoogleCredentialFailure + @brief Tests the flow of a failed @c signInWithCredential:completion: call with an + Google Sign-In credential. + */ +- (void)testSignInWithGoogleCredentialFailure { + OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils emailAlreadyInUseErrorWithEmail:kGoogleEmail]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *googleCredential = + [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken]; + [[FIRAuth auth] signInWithCredential:googleCredential completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(user); + XCTAssertEqual(error.code, FIRAuthErrorCodeEmailAlreadyInUse); + XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + XCTAssertNil([FIRAuth auth].currentUser); + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInAnonymouslySuccess + @brief Tests the flow of a successful @c signInAnonymously:completion: call. + */ +- (void)testSignInAnonymouslySuccess { + OCMExpect([_mockBackend signUpNewUser:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSignUpNewUserRequest *_Nullable request, + FIRSignupNewUserCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertNil(request.email); + XCTAssertNil(request.password); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSignUpNewUserResponse = OCMClassMock([FIRSignUpNewUserResponse class]); + [self stubTokensWithMockResponse:mockSignUpNewUserResponse]; + callback(mockSignUpNewUserResponse, nil); + }); + }); + [self expectGetAccountInfoAnonymous]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] signInAnonymouslyWithCompletion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserAnonymous:user]; + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUserAnonymous:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInAnonymouslyFailure + @brief Tests the flow of a failed @c signInAnonymously:completion: call. + */ +- (void)testSignInAnonymouslyFailure { + OCMExpect([_mockBackend signUpNewUser:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils operationNotAllowedErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] signInAnonymouslyWithCompletion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(user); + XCTAssertEqual(error.code, FIRAuthErrorCodeOperationNotAllowed); + XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + XCTAssertNil([FIRAuth auth].currentUser); + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInWithCustomTokenSuccess + @brief Tests the flow of a successful @c signInWithCustomToken:completion: call. + */ +- (void)testSignInWithCustomTokenSuccess { + OCMExpect([_mockBackend verifyCustomToken:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyCustomTokenRequest *_Nullable request, + FIRVerifyCustomTokenResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.token, kCustomToken); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVeriyCustomTokenResponse = OCMClassMock([FIRVerifyCustomTokenResponse class]); + [self stubTokensWithMockResponse:mockVeriyCustomTokenResponse]; + callback(mockVeriyCustomTokenResponse, nil); + }); + }); + [self expectGetAccountInfo]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] signInWithCustomToken:kCustomToken completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUser:user]; + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUser:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInWithCustomTokenFailure + @brief Tests the flow of a failed @c signInWithCustomToken:completion: call. + */ +- (void)testSignInWithCustomTokenFailure { + OCMExpect([_mockBackend verifyCustomToken:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils invalidCustomTokenErrorWithMessage:nil]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] signInWithCustomToken:kCustomToken completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(user); + XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidCustomToken); + XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + XCTAssertNil([FIRAuth auth].currentUser); + OCMVerifyAll(_mockBackend); +} + +/** @fn testCreateUserWithEmailPasswordSuccess + @brief Tests the flow of a successful @c createUserWithEmail:password:completion: call. + */ +- (void)testCreateUserWithEmailPasswordSuccess { + OCMExpect([_mockBackend signUpNewUser:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSignUpNewUserRequest *_Nullable request, + FIRSignupNewUserCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.email, kEmail); + XCTAssertEqualObjects(request.password, kPassword); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSignUpNewUserResponse = OCMClassMock([FIRSignUpNewUserResponse class]); + [self stubTokensWithMockResponse:mockSignUpNewUserResponse]; + callback(mockSignUpNewUserResponse, nil); + }); + }); + [self expectGetAccountInfo]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] createUserWithEmail:kEmail + password:kPassword + completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUser:user]; + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUser:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testCreateUserWithEmailPasswordFailure + @brief Tests the flow of a failed @c createUserWithEmail:password:completion: call. + */ +- (void)testCreateUserWithEmailPasswordFailure { + NSString *reason = @"Password shouldn't be a common word."; + OCMExpect([_mockBackend signUpNewUser:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils weakPasswordErrorWithServerResponseReason:reason]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] createUserWithEmail:kEmail + password:kPassword + completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(user); + XCTAssertEqual(error.code, FIRAuthErrorCodeWeakPassword); + XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]); + XCTAssertEqualObjects(error.userInfo[NSLocalizedFailureReasonErrorKey], reason); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + XCTAssertNil([FIRAuth auth].currentUser); + OCMVerifyAll(_mockBackend); +} + +/** @fn testCreateUserEmptyPasswordFailure + @brief Tests the flow of a failed @c createUserWithEmail:password:completion: call due to an + empty password. This error occurs on the client side, so there is no need to fake an RPC + response. + */ +- (void)testCreateUserEmptyPasswordFailure { + [self expectGetAccountInfo]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] createUserWithEmail:kEmail + password:@"" + completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(error.code, FIRAuthErrorCodeWeakPassword); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + +/** @fn testSendPasswordResetEmailSuccess + @brief Tests the flow of a successful @c sendPasswordResetWithEmail:completion: call. + */ +- (void)testSendPasswordResetEmailSuccess { + OCMExpect([_mockBackend getOOBConfirmationCode:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetOOBConfirmationCodeRequest *_Nullable request, + FIRGetOOBConfirmationCodeResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.email, kEmail); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback([[FIRGetOOBConfirmationCodeResponse alloc] init], nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] sendPasswordResetWithEmail:kEmail completion:^(NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSendPasswordResetEmailFailure + @brief Tests the flow of a failed @c sendPasswordResetWithEmail:completion: call. + */ +- (void)testSendPasswordResetEmailFailure { + OCMExpect([_mockBackend getOOBConfirmationCode:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils appNotAuthorizedError]); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] sendPasswordResetWithEmail:kEmail completion:^(NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(error.code, FIRAuthErrorCodeAppNotAuthorized); + XCTAssertNotNil(error.userInfo[NSLocalizedDescriptionKey]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignOut + @brief Tests the @c signOut: method. + */ +- (void)testSignOut { + [self waitForSignIn]; + // Verify signing out succeeds and clears the current user. + NSError *error; + XCTAssertTrue([[FIRAuth auth] signOut:&error]); + XCTAssertNil([FIRAuth auth].currentUser); +} + +/** @fn testAuthStateChanges + @brief Tests @c addAuthStateDidChangeListener: and @c removeAuthStateDidChangeListener: methods. + */ +- (void)testAuthStateChanges { + // Set up listener. + __block XCTestExpectation *expectation; + __block BOOL shouldHaveUser; + FIRAuthStateDidChangeListenerBlock listener = ^(FIRAuth *auth, FIRUser *_Nullable user) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(auth, [FIRAuth auth]); + XCTAssertEqual(user, [FIRAuth auth].currentUser); + if (shouldHaveUser) { + XCTAssertNotNil(user); + } else { + XCTAssertNil(user); + } + // `expectation` being nil means the listener is not expected to be fired at this moment. + XCTAssertNotNil(expectation); + [expectation fulfill]; + }; + [[FIRAuth auth] signOut:NULL]; + [self waitForTimeIntervel:kWaitInterval]; // Wait until dust settled from previous tests. + + // Listener should fire immediately when attached. + expectation = [self expectationWithDescription:@"initial"]; + shouldHaveUser = NO; + FIRAuthStateDidChangeListenerHandle handle = + [[FIRAuth auth] addAuthStateDidChangeListener:listener]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + + // Listener should fire for signing in. + expectation = [self expectationWithDescription:@"sign-in"]; + shouldHaveUser = YES; + [self waitForSignIn]; + + // Listener should not fire for signing in again. + shouldHaveUser = YES; + [self waitForSignIn]; + [self waitForTimeIntervel:kWaitInterval]; // make sure listener is not called + + // Listener should fire for signing out. + expectation = [self expectationWithDescription:@"sign-out"]; + shouldHaveUser = NO; + [[FIRAuth auth] signOut:NULL]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + + // Listener should no longer fire once detached. + expectation = nil; + [[FIRAuth auth] removeAuthStateDidChangeListener:handle]; + [self waitForSignIn]; + [self waitForTimeIntervel:kWaitInterval]; // make sure listener is no longer called +} + +/** @fn testIDTokenChanges + @brief Tests @c addIDTokenDidChangeListener: and @c removeIDTokenDidChangeListener: methods. + */ +- (void)testIDTokenChanges { + // Set up listener. + __block XCTestExpectation *expectation; + __block BOOL shouldHaveUser; + FIRIDTokenDidChangeListenerBlock listener = ^(FIRAuth *auth, FIRUser *_Nullable user) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertEqual(auth, [FIRAuth auth]); + XCTAssertEqual(user, [FIRAuth auth].currentUser); + if (shouldHaveUser) { + XCTAssertNotNil(user); + } else { + XCTAssertNil(user); + } + // `expectation` being nil means the listener is not expected to be fired at this moment. + XCTAssertNotNil(expectation); + [expectation fulfill]; + }; + [[FIRAuth auth] signOut:NULL]; + [self waitForTimeIntervel:kWaitInterval]; // Wait until dust settled from previous tests. + + // Listener should fire immediately when attached. + expectation = [self expectationWithDescription:@"initial"]; + shouldHaveUser = NO; + FIRIDTokenDidChangeListenerHandle handle = + [[FIRAuth auth] addIDTokenDidChangeListener:listener]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + + // Listener should fire for signing in. + expectation = [self expectationWithDescription:@"sign-in"]; + shouldHaveUser = YES; + [self waitForSignIn]; + + // Listener should fire for signing in again as the same user. + expectation = [self expectationWithDescription:@"sign-in again"]; + shouldHaveUser = YES; + [self waitForSignIn]; + + // Listener should fire for signing out. + expectation = [self expectationWithDescription:@"sign-out"]; + shouldHaveUser = NO; + [[FIRAuth auth] signOut:NULL]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + + // Listener should no longer fire once detached. + expectation = nil; + [[FIRAuth auth] removeIDTokenDidChangeListener:handle]; + [self waitForSignIn]; + [self waitForTimeIntervel:kWaitInterval]; // make sure listener is no longer called +} + +#pragma mark - Automatic Token Refresh Tests. + +/** @fn testAutomaticTokenRefresh + @brief Tests a successful flow to automatically refresh tokens for a signed in user. + */ +- (void)testAutomaticTokenRefresh { + [[FIRAuth auth] signOut:NULL]; + + // Enable auto refresh + [self enableAutoTokenRefresh]; + + // Sign in a user. + [self waitForSignIn]; + + // Set up expectation for secureToken RPC made by token refresh task. + [self mockSecureTokenResponseWithError:nil]; + + // Verify that the current user's access token is the "old" access token before automatic token + // refresh. + XCTAssertEqualObjects(kAccessToken, [FIRAuth auth].currentUser.rawAccessToken); + + // Execute saved token refresh task. + XCTestExpectation *dispatchAfterExpectation = + [self expectationWithDescription:@"dispatchAfterExpectation"]; + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + XCTAssertNotNil(_FIRAuthDispatcherCallback); + _FIRAuthDispatcherCallback(); + [dispatchAfterExpectation fulfill]; + }); + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + + // Verify that current user's access token is the "new" access token provided in the mock secure + // token response during automatic token refresh. + XCTAssertEqualObjects(kNewAccessToken, [FIRAuth auth].currentUser.rawAccessToken); + OCMVerifyAll(_mockBackend); +} + +/** @fn testAutomaticTokenRefreshInvalidTokenFailure + @brief Tests an unsuccessful flow to auto refresh tokens with an "invalid token" error. + This error should cause the user to be signed out. + */ +- (void)testAutomaticTokenRefreshInvalidTokenFailure { + [[FIRAuth auth] signOut:NULL]; + // Enable auto refresh + [self enableAutoTokenRefresh]; + + // Sign in a user. + [self waitForSignIn]; + + // Set up expectation for secureToken RPC made by a failed attempt to refresh tokens. + [self mockSecureTokenResponseWithError:[FIRAuthErrorUtils invalidUserTokenErrorWithMessage:nil]]; + + // Verify that current user is still valid. + XCTAssertEqualObjects(kAccessToken, [FIRAuth auth].currentUser.rawAccessToken); + + // Execute saved token refresh task. + XCTestExpectation *dispatchAfterExpectation = + [self expectationWithDescription:@"dispatchAfterExpectation"]; + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + XCTAssertNotNil(_FIRAuthDispatcherCallback); + _FIRAuthDispatcherCallback(); + [dispatchAfterExpectation fulfill]; + }); + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + + //Verify that the user is nil after failed attempt to refresh tokens caused signed out. + XCTAssertNil([FIRAuth auth].currentUser); + OCMVerifyAll(_mockBackend); +} + +/** @fn testAutomaticTokenRefreshRetry + @brief Tests that a retry is attempted for a automatic token refresh task (which is not due to + invalid tokens). The initial attempt to refresh the access token fails, but the second + attempt is successful. + */ +- (void)testAutomaticTokenRefreshRetry { + [[FIRAuth auth] signOut:NULL]; + // Enable auto refresh + [self enableAutoTokenRefresh]; + + // Sign in a user. + [self waitForSignIn]; + + // Set up expectation for secureToken RPC made by a failed attempt to refresh tokens. + [self mockSecureTokenResponseWithError:[NSError errorWithDomain:@"ERROR" code:-1 userInfo:nil]]; + + // Execute saved token refresh task. + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + XCTAssertNotNil(_FIRAuthDispatcherCallback); + _FIRAuthDispatcherCallback(); + _FIRAuthDispatcherCallback = nil; + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + + // The old access token should still be the current user's access token and not the new access + // token (kNewAccessToken). + XCTAssertEqualObjects(kAccessToken, [FIRAuth auth].currentUser.rawAccessToken); + + // Set up expectation for secureToken RPC made by a successful attempt to refresh tokens. + [self mockSecureTokenResponseWithError:nil]; + + // Execute saved token refresh task. + XCTestExpectation *dispatchAfterExpectation = + [self expectationWithDescription:@"dispatchAfterExpectation"]; + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + XCTAssertNotNil(_FIRAuthDispatcherCallback); + _FIRAuthDispatcherCallback(); + [dispatchAfterExpectation fulfill]; + }); + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + + // Verify that current user's access token is the "new" access token provided in the mock secure + // token response during automatic token refresh. + XCTAssertEqualObjects([FIRAuth auth].currentUser.rawAccessToken, kNewAccessToken); + OCMVerifyAll(_mockBackend); +} + +/** @fn testAutomaticTokenRefreshInvalidTokenFailure + @brief Tests that app foreground notification triggers the scheduling of an automatic token + refresh task. + */ +- (void)testAutoRefreshAppForegroundedNotification { + [[FIRAuth auth] signOut:NULL]; + // Enable auto refresh + [self enableAutoTokenRefresh]; + + // Sign in a user. + [self waitForSignIn]; + + // Post "UIApplicationDidBecomeActiveNotification" to trigger scheduling token refresh task. + [[NSNotificationCenter defaultCenter] + postNotificationName:UIApplicationDidBecomeActiveNotification object:nil]; + + // Verify that current user is still valid with old access token. + XCTAssertEqualObjects(kAccessToken, [FIRAuth auth].currentUser.rawAccessToken); + + // Set up expectation for secureToken RPC made by a successful attempt to refresh tokens. + [self mockSecureTokenResponseWithError:nil]; + + // Execute saved token refresh task. + XCTestExpectation *dispatchAfterExpectation = + [self expectationWithDescription:@"dispatchAfterExpectation"]; + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + XCTAssertNotNil(_FIRAuthDispatcherCallback); + _FIRAuthDispatcherCallback(); + [dispatchAfterExpectation fulfill]; + }); + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + // Verify that current user is still valid with new access token. + XCTAssertEqualObjects(kNewAccessToken, [FIRAuth auth].currentUser.rawAccessToken); + OCMVerifyAll(_mockBackend); +} + +#pragma mark - Helpers + +/** @fn mockSecureTokenResponseWithError: + @brief Set up expectation for secureToken RPC. + @param error The error that the mock should return if any. + */ +- (void)mockSecureTokenResponseWithError:(nullable NSError *)error { + // Set up expectation for secureToken RPC made by a successful attempt to refresh tokens. + XCTestExpectation *secureTokenResponseExpectation = + [self expectationWithDescription:@"secureTokenResponseExpectation"]; + OCMExpect([_mockBackend secureToken:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSecureTokenRequest *_Nullable request, + FIRSecureTokenResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.refreshToken, kRefreshToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + if (error) { + callback(nil, error); + [secureTokenResponseExpectation fulfill]; + return; + } + id mockSecureTokenResponse = OCMClassMock([FIRSecureTokenResponse class]); + OCMStub([mockSecureTokenResponse accessToken]).andReturn(kNewAccessToken); + NSDate *futureDate = + [[NSDate date] dateByAddingTimeInterval:kTestTokenExpirationTimeInterval]; + OCMStub([mockSecureTokenResponse approximateExpirationDate]).andReturn(futureDate); + callback(mockSecureTokenResponse, nil); + [secureTokenResponseExpectation fulfill]; + }); + }); +} + +/** @fn enableAutoTokenRefresh + @brief Enables automatic token refresh by invoking FIRAuth's implementation of FIRApp's + |getTokenWithImplementation|. + */ +- (void)enableAutoTokenRefresh { + XCTestExpectation *expectation = [self expectationWithDescription:@"autoTokenRefreshcallback"]; + [[FIRAuth auth].app getTokenForcingRefresh:NO withCallback:^(NSString *_Nullable token, + NSError *_Nullable error) { + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + +/** @fn app1 + @brief Creates a Firebase app. + @return A @c FIRApp with some name. + */ +- (FIRApp *)app1 { + return [FIRApp appForAuthUnitTestsWithName:kFirebaseAppName1]; +} + +/** @fn app2 + @brief Creates another Firebase app. + @return A @c FIRApp with some other name. + */ +- (FIRApp *)app2 { + return [FIRApp appForAuthUnitTestsWithName:kFirebaseAppName2]; +} + +/** @fn stubSecureTokensWithMockResponse + @brief Creates stubs on the mock response object with access and refresh tokens + @param mockResponse The mock response object. + */ +- (void)stubTokensWithMockResponse:(id)mockResponse { + OCMStub([mockResponse IDToken]).andReturn(kAccessToken); + OCMStub([mockResponse approximateExpirationDate]) + .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]); + OCMStub([mockResponse refreshToken]).andReturn(kRefreshToken); +} + +/** @fn expectGetAccountInfo + @brief Expects a GetAccountInfo request on the mock backend and calls back with fake account + data. + */ +- (void)expectGetAccountInfo { + OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request, + FIRGetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kDisplayName); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]); + OCMStub([mockGetAccountInfoResponse users]).andReturn(@[ mockGetAccountInfoResponseUser ]); + callback(mockGetAccountInfoResponse, nil); + }); + }); +} + +/** @fn assertUser + @brief Asserts the given FIRUser matching the fake data returned by @c expectGetAccountInfo. + @param user The user object to be verified. + */ +- (void)assertUser:(FIRUser *)user { + XCTAssertNotNil(user); + XCTAssertEqualObjects(user.uid, kLocalID); + XCTAssertEqualObjects(user.displayName, kDisplayName); + XCTAssertEqualObjects(user.email, kEmail); + XCTAssertFalse(user.anonymous); + XCTAssertEqual(user.providerData.count, 0u); +} + +/** @fn expectGetAccountInfoGoogle + @brief Expects a GetAccountInfo request on the mock backend and calls back with fake account + data for a Google Sign-In user. + */ +- (void)expectGetAccountInfoGoogle { + OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request, + FIRGetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGoogleUserInfo = OCMClassMock([FIRGetAccountInfoResponseProviderUserInfo class]); + OCMStub([mockGoogleUserInfo providerID]).andReturn(FIRGoogleAuthProviderID); + OCMStub([mockGoogleUserInfo displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGoogleUserInfo federatedID]).andReturn(kGoogleID); + OCMStub([mockGoogleUserInfo email]).andReturn(kGoogleEmail); + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kDisplayName); + OCMStub([mockGetAccountInfoResponseUser providerUserInfo]) + .andReturn((@[ mockGoogleUserInfo ])); + id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]); + OCMStub([mockGetAccountInfoResponse users]).andReturn(@[ mockGetAccountInfoResponseUser ]); + callback(mockGetAccountInfoResponse, nil); + }); + }); +} + +/** @fn assertUserGoogle + @brief Asserts the given FIRUser matching the fake data returned by + @c expectGetAccountInfoGoogle. + @param user The user object to be verified. + */ +- (void)assertUserGoogle:(FIRUser *)user { + XCTAssertNotNil(user); + XCTAssertEqualObjects(user.uid, kLocalID); + XCTAssertEqualObjects(user.displayName, kDisplayName); + XCTAssertEqual(user.providerData.count, 1u); + id<FIRUserInfo> googleUserInfo = user.providerData[0]; + XCTAssertEqualObjects(googleUserInfo.providerID, FIRGoogleAuthProviderID); + XCTAssertEqualObjects(googleUserInfo.uid, kGoogleID); + XCTAssertEqualObjects(googleUserInfo.displayName, kGoogleDisplayName); + XCTAssertEqualObjects(googleUserInfo.email, kGoogleEmail); +} + +/** @fn expectGetAccountInfoAnonymous + @brief Expects a GetAccountInfo request on the mock backend and calls back with fake anonymous + account data. + */ +- (void)expectGetAccountInfoAnonymous { + OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request, + FIRGetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]); + OCMStub([mockGetAccountInfoResponse users]).andReturn(@[ mockGetAccountInfoResponseUser ]); + callback(mockGetAccountInfoResponse, nil); + }); + }); +} + +/** @fn assertUserAnonymous + @brief Asserts the given FIRUser matching the fake data returned by + @c expectGetAccountInfoAnonymous. + @param user The user object to be verified. + */ +- (void)assertUserAnonymous:(FIRUser *)user { + XCTAssertNotNil(user); + XCTAssertEqualObjects(user.uid, kLocalID); + XCTAssertNil(user.displayName); + XCTAssertTrue(user.anonymous); + XCTAssertEqual(user.providerData.count, 0u); +} + +/** @fn waitForSignIn + @brief Signs in a user to prepare for tests. + @remarks This method also waits for all other pending @c XCTestExpectation instances. + */ +- (void)waitForSignIn { + OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request, + FIRVerifyPasswordResponseCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVeriyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]); + [self stubTokensWithMockResponse:mockVeriyPasswordResponse]; + callback(mockVeriyPasswordResponse, nil); + }); + }); + [self expectGetAccountInfo]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signInWithEmail:kEmail password:kPassword completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + XCTAssertNotNil([FIRAuth auth].currentUser); +} + +/** @fn waitForTimeInterval: + @brief Wait for a particular time interval. + @remarks This method also waits for all other pending @c XCTestExpectation instances. + */ +- (void)waitForTimeIntervel:(NSTimeInterval)timeInterval { + static dispatch_queue_t queue; + static dispatch_once_t onceToken; + XCTestExpectation *expectation = [self expectationWithDescription:@"waitForTimeIntervel:"]; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create("com.google.FIRAuthUnitTests.waitForTimeIntervel", NULL); + }); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeInterval * NSEC_PER_SEC), queue, ^() { + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:timeInterval + kExpectationTimeout handler:nil]; +} + +@end diff --git a/Example/Auth/Tests/FIRAuthUserDefaultsStorageTests.m b/Example/Auth/Tests/FIRAuthUserDefaultsStorageTests.m new file mode 100644 index 0000000..07493d5 --- /dev/null +++ b/Example/Auth/Tests/FIRAuthUserDefaultsStorageTests.m @@ -0,0 +1,155 @@ +/* + * Copyright 2017 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 "FIRAuthUserDefaultsStorage.h" + +#if FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE + +NS_ASSUME_NONNULL_BEGIN + +/** @var kKey + @brief The key used in tests. + */ +static NSString *const kKey = @"ACCOUNT"; + +/** @var kService + @brief The keychain service used in tests. + */ +static NSString *const kService = @"SERVICE"; + +/** @var kOtherService + @brief Another keychain service used in tests. + */ +static NSString *const kOtherService = @"OTHER_SERVICE"; + +/** @var kData + @brief A piece of keychain data used in tests. + */ +static NSString *const kData = @"DATA"; + +/** @var kOtherData + @brief Another piece of keychain data used in tests. + */ +static NSString *const kOtherData = @"OTHER_DATA"; + +/** @fn dataFromString + @brief Converts a NSString to NSData. + @param string The NSString to be converted from. + @return The NSData being the conversion result. + */ +static NSData *dataFromString(NSString *string) { + return [string dataUsingEncoding:NSUTF8StringEncoding]; +} + +/** @fn fakeError + @brief Creates a fake error object. + @return a non-nil NSError instance. + */ +static NSError *fakeError() { + return [NSError errorWithDomain:@"ERROR" code:-1 userInfo:nil]; +} + +/** @class FIRAuthUserDefaultsStorageTests + @brief Tests for @c FIRAuthUserDefaultsStorage . + */ +@interface FIRAuthUserDefaultsStorageTests : XCTestCase +@end + +@implementation FIRAuthUserDefaultsStorageTests { + /** @var _storage + @brief The @c FIRAuthUserDefaultsStorage object under test. + */ + FIRAuthUserDefaultsStorage *_storage; +} + +- (void)setUp { + [super setUp]; + _storage = [[FIRAuthUserDefaultsStorage alloc] initWithService:kService]; + [_storage clear]; +} + +/** @fn testReadNonexisting + @brief Tests reading non-existing storage item. + */ +- (void)testReadNonExisting { + NSError *error = fakeError(); + XCTAssertNil([_storage dataForKey:kKey error:&error]); + XCTAssertNil(error); +} + +/** @fn testWriteRead + @brief Tests writing and reading a storage item. + */ +- (void)testWriteRead { + XCTAssertTrue([_storage setData:dataFromString(kData) forKey:kKey error:NULL]); + NSError *error = fakeError(); + XCTAssertEqualObjects([_storage dataForKey:kKey error:&error], dataFromString(kData)); + XCTAssertNil(error); +} + +/** @fn testOverwrite + @brief Tests overwriting a storage item. + */ +- (void)testOverwrite { + XCTAssertTrue([_storage setData:dataFromString(kData) forKey:kKey error:NULL]); + XCTAssertTrue([_storage setData:dataFromString(kOtherData) forKey:kKey error:NULL]); + NSError *error = fakeError(); + XCTAssertEqualObjects([_storage dataForKey:kKey error:&error], dataFromString(kOtherData)); + XCTAssertNil(error); +} + +/** @fn testRemove + @brief Tests removing a storage item. + */ +- (void)testRemove { + XCTAssertTrue([_storage setData:dataFromString(kData) forKey:kKey error:NULL]); + XCTAssertTrue([_storage removeDataForKey:kKey error:NULL]); + NSError *error = fakeError(); + XCTAssertNil([_storage dataForKey:kKey error:&error]); + XCTAssertNil(error); +} + +/** @fn testServices + @brief Tests storage items belonging to different services doesn't affect each other. + */ +- (void)testServices { + XCTAssertTrue([_storage setData:dataFromString(kData) forKey:kKey error:NULL]); + _storage = [[FIRAuthUserDefaultsStorage alloc] initWithService:kOtherService]; + NSError *error = fakeError(); + XCTAssertNil([_storage dataForKey:kKey error:&error]); + XCTAssertNil(error); +} + +/** @fn testStandardUserDefaults + @brief Tests standard user defaults are not affected by FIRAuthUserDefaultsStorage operations, + */ +- (void)testStandardUserDefaults { + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + NSUInteger count = + [userDefaults persistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]].count; + XCTAssertTrue([_storage setData:dataFromString(kData) forKey:kKey error:NULL]); + XCTAssertNil([userDefaults dataForKey:kKey]); + XCTAssertEqual([userDefaults persistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]] + .count, count); +} + +@end + +NS_ASSUME_NONNULL_END + +#endif // FIRAUTH_USER_DEFAULTS_STORAGE_AVAILABLE diff --git a/Example/Auth/Tests/FIRCreateAuthURIRequestTests.m b/Example/Auth/Tests/FIRCreateAuthURIRequestTests.m new file mode 100644 index 0000000..409c232 --- /dev/null +++ b/Example/Auth/Tests/FIRCreateAuthURIRequestTests.m @@ -0,0 +1,103 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRCreateAuthURIRequest.h" +#import "FIRCreateAuthURIResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kTestAuthUri + @brief The test value of the "authURI" property in the json response. + */ +static NSString *const kTestAuthUri = @"AuthURI"; + +/** @var kTestIdentifier + @brief Fake identifier key used for testing. + */ +static NSString *const kTestIdentifier = @"Identifier"; + +/** @var kContinueURITestKey + @brief The key for the "continueUri" value in the request. + */ +static NSString *const kContinueURITestKey = @"continueUri"; + +/** @var kTestContinueURI + @brief Fake Continue URI key used for testing. + */ +static NSString *const kTestContinueURI = @"ContinueUri"; + +/** @var kExpectedAPIURL + @brief The expected URL for the test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/createAuthUri?key=APIKey"; + +/** @class FIRCreateAuthURIRequestTests + @brief Tests for @c CreateAuthURIRequest. + */ +@interface FIRCreateAuthURIRequestTests : XCTestCase +@end +@implementation FIRCreateAuthURIRequestTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testCreateAuthUriRequest + @brief Tests the encoding of an create auth URI request. + */ +- (void)testEmailVerificationRequest { + FIRCreateAuthURIRequest *request = + [[FIRCreateAuthURIRequest alloc]initWithIdentifier:kTestIdentifier + continueURI:kTestContinueURI + APIKey:kTestAPIKey]; + + [FIRAuthBackend createAuthURI:request + callback:^(FIRCreateAuthURIResponse *_Nullable response, + NSError *_Nullable error) { + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kContinueURITestKey], kTestContinueURI); +} + + +@end diff --git a/Example/Auth/Tests/FIRCreateAuthURIResponseTests.m b/Example/Auth/Tests/FIRCreateAuthURIResponseTests.m new file mode 100644 index 0000000..11cab9d --- /dev/null +++ b/Example/Auth/Tests/FIRCreateAuthURIResponseTests.m @@ -0,0 +1,205 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRCreateAuthURIRequest.h" +#import "FIRCreateAuthURIResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kAuthUriKey + @brief The name of the "authURI" property in the json response. + */ +static NSString *const kAuthUriKey = @"authUri"; + +/** @var kTestAuthUri + @brief The test value of the "authURI" property in the json response. + */ +static NSString *const kTestAuthUri = @"AuthURI"; + +/** @var kTestIdentifier + @brief Fake identifier key used for testing. + */ +static NSString *const kTestIdentifier = @"Identifier"; + +/** @var kTestContinueURI + @brief Fake Continue URI key used for testing. + */ +static NSString *const kTestContinueURI = @"ContinueUri"; + +/** @var kMissingContinueURIErrorMessage + @brief The error returned by the server if continue Uri is invalid. + */ +static NSString *const kMissingContinueURIErrorMessage = @"MISSING_CONTINUE_URI:"; + +/** @var kInvalidEmailErrorMessage + @brief The error returned by the server if the email is invalid. + */ +static NSString *const kInvalidIdentifierErrorMessage = @"INVALID_IDENTIFIER :"; + +/** @var kInvalidEmailErrorMessage + @brief The error returned by the server if the email is invalid. + */ +static NSString *const kInvalidEmailErrorMessage = @"INVALID_EMAIL:"; + +/** @class CreateAuthURIResponseTests + @brief Tests for @c FIRCreateAuthURIResponse. + */ +@interface FIRCreateAuthURIResponseTests : XCTestCase +@end +@implementation FIRCreateAuthURIResponseTests{ + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testMissingContinueURIError + @brief This test checks for invalid continue URI in the response. + */ +- (void)testMissingContinueURIError { + FIRCreateAuthURIRequest *request = + [[FIRCreateAuthURIRequest alloc]initWithIdentifier:kTestIdentifier + continueURI:kTestContinueURI + APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRCreateAuthURIResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend createAuthURI:request + callback:^(FIRCreateAuthURIResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kMissingContinueURIErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInternalError); + XCTAssertNil(RPCResponse); +} + +/** @fn testInvalidIdentifierError + @brief This test checks for the INVALID_IDENTIFIER error message from the backend. + */ +- (void)testInvalidIdentifierError { + FIRCreateAuthURIRequest *request = + [[FIRCreateAuthURIRequest alloc]initWithIdentifier:kTestIdentifier + continueURI:kTestContinueURI + APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRCreateAuthURIResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend createAuthURI:request + callback:^(FIRCreateAuthURIResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kInvalidIdentifierErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidEmail); + XCTAssertNil(RPCResponse); +} + +/** @fn testInvalidEmailError + @brief This test checks for INVALID_EMAIL error message from the backend. + */ +- (void)testInvalidEmailError { + FIRCreateAuthURIRequest *request = + [[FIRCreateAuthURIRequest alloc]initWithIdentifier:kTestIdentifier + continueURI:kTestContinueURI + APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRCreateAuthURIResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend createAuthURI:request + callback:^(FIRCreateAuthURIResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kInvalidEmailErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidEmail); + XCTAssertNil(RPCResponse); +} + +/** @fn testSuccessfulCreateAuthURI + @brief This test checks for invalid email identifier error. + */ +- (void)testSuccessfulCreateAuthURIResponse { + FIRCreateAuthURIRequest *request = + [[FIRCreateAuthURIRequest alloc]initWithIdentifier:kTestIdentifier + continueURI:kTestContinueURI + APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRCreateAuthURIResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend createAuthURI:request + callback:^(FIRCreateAuthURIResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + kAuthUriKey : kTestAuthUri + }]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.authURI, kTestAuthUri); +} + +@end diff --git a/Example/Auth/Tests/FIRDeleteAccountRequestTests.m b/Example/Auth/Tests/FIRDeleteAccountRequestTests.m new file mode 100644 index 0000000..05f1d47 --- /dev/null +++ b/Example/Auth/Tests/FIRDeleteAccountRequestTests.m @@ -0,0 +1,98 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRDeleteAccountRequest.h" +#import "FIRDeleteAccountResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kLocalID + @brief Fake LocalID used for testing. + */ +static NSString *const kLocalID = @"LocalID"; + +/** @var kLocalIDKey + @brief The name of the "localID" property in the request. + */ +static NSString *const kLocalIDKey = @"localId"; + +/** @var kAccessToken + @brief The name of the "AccessToken" property in the request. + */ +static NSString *const kAccessToken = @"AccessToken"; + +/** @var kExpectedAPIURL + @brief The expected URL for test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/deleteAccount?key=APIKey"; + +/** @class FIRDeleteUserRequestTests + @brief Tests for @c FIRDeleteAccountRequest. + */ +@interface FIRDeleteAccountRequestTests : XCTestCase +@end +@implementation FIRDeleteAccountRequestTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testDeleteAccountRequest + @brief Tests the delete account request. + */ +- (void)testDeleteAccountRequest { + + FIRDeleteAccountRequest *request = [[FIRDeleteAccountRequest alloc] initWithAPIKey:kTestAPIKey + localID:kLocalID + accessToken:kAccessToken]; + __block BOOL callbackInvoked; + __block NSError *RPCError; + [FIRAuthBackend deleteAccount:request + callback:^(NSError *_Nullable error) { + callbackInvoked = YES; + RPCError = error; + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertNotNil(_RPCIssuer.decodedRequest[kLocalIDKey]); +} + +@end diff --git a/Example/Auth/Tests/FIRDeleteAccountResponseTests.m b/Example/Auth/Tests/FIRDeleteAccountResponseTests.m new file mode 100644 index 0000000..f75735e --- /dev/null +++ b/Example/Auth/Tests/FIRDeleteAccountResponseTests.m @@ -0,0 +1,172 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRDeleteAccountRequest.h" +#import "FIRDeleteAccountResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kLocalID + @brief Fake LocalID used for testing. + */ +static NSString *const kLocalID = @"LocalID"; + +/** @var kAccessToken + @brief Fake AccessToken used for testing. + */ +static NSString *const kAccessToken = @"AccessToken"; + +/** @var kUserDisabledErrorMessage + @brief The error returned by the server if the user account is diabled. + */ +static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED"; + +/** @var kinvalidUserTokenErrorMessage + @brief This is the error message the server responds with if user's saved auth credential is + invalid, and the user needs to sign in again. + */ +static NSString *const kinvalidUserTokenErrorMessage = @"INVALID_ID_TOKEN:"; + +/** @var kCredentialTooOldErrorMessage + @brief This is the error message the server responds with if account change is attempted 5 + minutes after signing in. + */ +static NSString *const kCredentialTooOldErrorMessage = @"CREDENTIAL_TOO_OLD_LOGIN_AGAIN:"; + +/** @class FIRDeleteUserResponseTests + @brief Tests for @c FIRDeleteAccountResponse. + */ +@interface FIRDeleteAccountResponseTests : XCTestCase +@end +@implementation FIRDeleteAccountResponseTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testUserDisabledError + @brief This test simulates the occurrence of a @c userDisabled error. + */ +- (void)testUserDisabledError { + FIRDeleteAccountRequest *request = [[FIRDeleteAccountRequest alloc] initWithAPIKey:kTestAPIKey + localID:kLocalID + accessToken:kAccessToken]; + + __block BOOL callbackInvoked; + __block NSError *RPCError; + [FIRAuthBackend deleteAccount:request + callback:^(NSError *_Nullable error) { + callbackInvoked = YES; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kUserDisabledErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserDisabled); +} + +/** @fn testinvalidUserTokenError + @brief This test simulates the occurrence of a @c invalidUserToken error. + */ +- (void)testinvalidUserTokenError { + FIRDeleteAccountRequest *request = [[FIRDeleteAccountRequest alloc] initWithAPIKey:kTestAPIKey + localID:kLocalID + accessToken:kAccessToken]; + + __block BOOL callbackInvoked; + __block NSError *RPCError; + [FIRAuthBackend deleteAccount:request + callback:^(NSError *_Nullable error) { + callbackInvoked = YES; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kinvalidUserTokenErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidUserToken); +} + +/** @fn testrequiredRecentLoginError + @brief This test simulates the occurrence of a @c credentialTooOld error. + */ +- (void)testrequiredRecentLoginError { + FIRDeleteAccountRequest *request = [[FIRDeleteAccountRequest alloc] initWithAPIKey:kTestAPIKey + localID:kLocalID + accessToken:kAccessToken]; + __block BOOL callbackInvoked; + __block NSError *RPCError; + [FIRAuthBackend deleteAccount:request + callback:^(NSError *_Nullable error) { + callbackInvoked = YES; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kCredentialTooOldErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeRequiresRecentLogin); +} + +/** @fn testSuccessfulDeleteAccount + @brief This test simulates a completed succesful deleteAccount operation. + */ +- (void)testSuccessfulDeleteAccountResponse { + FIRDeleteAccountRequest *request = [[FIRDeleteAccountRequest alloc] initWithAPIKey:kTestAPIKey + localID:kLocalID + accessToken:kAccessToken]; + __block BOOL callbackInvoked; + __block NSError *RPCError; + [FIRAuthBackend deleteAccount:request + callback:^(NSError *_Nullable error) { + callbackInvoked = YES; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{}]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); +} + +@end diff --git a/Example/Auth/Tests/FIRFakeBackendRPCIssuer.h b/Example/Auth/Tests/FIRFakeBackendRPCIssuer.h new file mode 100644 index 0000000..d192cda --- /dev/null +++ b/Example/Auth/Tests/FIRFakeBackendRPCIssuer.h @@ -0,0 +1,100 @@ +/* + * Copyright 2017 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 "FIRAuthBackend.h" + +NS_ASSUME_NONNULL_BEGIN + +/** @class FIRFakeBackendRPCIssuer + @brief An implementation of @c FIRAuthBackendRPCIssuer which is used to test backend request, + response, and glue logic. + */ +@interface FIRFakeBackendRPCIssuer : NSObject <FIRAuthBackendRPCIssuer> + +/** @property requestURL + @brief The URL which was requested. + */ +@property(nonatomic, readonly) NSURL *requestURL; + +/** @property requestData + @brief The raw data in the POST body. + */ +@property(nonatomic, readonly) NSData *requestData; + +/** @property decodedRequest + @brief The raw data in the POST body decoded as JSON. + */ +@property(nonatomic, readonly) NSDictionary *decodedRequest; + +/** @property contentType + @brief The value of the content type HTTP header in the request. + */ +@property(nonatomic, readonly) NSString *contentType; + +/** @fn respondWithData:error: + @brief Responds to a pending RPC request with data and an error. + @remarks This is useful for simulating an error response with bogus data or unexpected data + (like unexpectedly receiving an HTML body.) + @param data The data to return as the body of an HTTP response. + @param error The simulated error to return from GTM. + */ +- (void)respondWithData:(nullable NSData *)data error:(nullable NSError *)error; + +/** @fn respondWithJSON:error: + @brief Responds to a pending RPC request with JSON and an error. + @remarks This is useful for simulating an error response with error JSON. + @param JSON The JSON to return. + @param error The simulated error to return from GTM. + */ +- (NSData *)respondWithJSON:(nullable NSDictionary *)JSON error:(nullable NSError *)error; + +/** @fn respondWithJSONError: + @brief Responds to a pending RPC request with a JSON server error. + @param JSON A dictionary which should be a server error encoded as JSON for fake response. + */ +- (NSData *)respondWithJSONError:(NSDictionary *)JSON; + +/** @fn respondWithError: + @brief Responds to a pending RPC request with an error. This is useful for simulating things + like a network timeout or unreachable host. + @param error The simulated error to return from GTM. + */ +- (NSData *)respondWithError:(NSError *)error; + +/** @fn respondWithServerErrorMessage:error: + @brief Responds to a pending RPC request with a server error message. + @param errorMessage The simulated error message to return from the server. + @param error The simulated error to return from GTM. + */ +- (NSData *)respondWithServerErrorMessage:(NSString *)errorMessage error:(NSError *)error; + +/** @fn respondWithServerErrorMessage: + @brief Responds to a pending RPC request with a server error message. + @param errorMessage The simulated error message to return from the server. + */ +- (NSData *)respondWithServerErrorMessage:(NSString *)errorMessage; + +/** @fn respondWithJSON: + @brief Responds to a pending RPC request with JSON. + @param JSON A dictionary which should be encoded as JSON for a fake response. + */ +- (NSData *)respondWithJSON:(NSDictionary *)JSON; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Auth/Tests/FIRFakeBackendRPCIssuer.m b/Example/Auth/Tests/FIRFakeBackendRPCIssuer.m new file mode 100644 index 0000000..93589e7 --- /dev/null +++ b/Example/Auth/Tests/FIRFakeBackendRPCIssuer.m @@ -0,0 +1,86 @@ +/* + * Copyright 2017 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 "FIRFakeBackendRPCIssuer.h" + +/** @var kFakeErrorDomain + @brief Fake error domain used for testing. + */ +static NSString *const kFakeErrorDomain = @"fake domain"; + +@implementation FIRFakeBackendRPCIssuer { + /** @var _handler + @brief A block we must invoke when @c respondWithError or @c respondWithJSON are called. + */ + FIRAuthBackendRPCIssuerCompletionHandler _handler; +} + +- (void)asyncPostToURL:(NSURL *)URL + body:(NSData *)body + contentType:(NSString *)contentType + completionHandler:(FIRAuthBackendRPCIssuerCompletionHandler)handler { + _requestURL = [URL copy]; + _requestData = body; + NSDictionary *JSON = [NSJSONSerialization JSONObjectWithData:body options:0 error:nil]; + _decodedRequest = JSON; + _contentType = contentType; + _handler = handler; +} + +- (void)respondWithData:(NSData *)data error:(NSError *)error { + NSAssert(_handler, @"There is no pending RPC request."); + NSAssert(data || error, @"At least one of: data or error should be been non-nil."); + FIRAuthBackendRPCIssuerCompletionHandler handler = _handler; + _handler = nil; + handler(data, error); +} + +- (NSData *)respondWithServerErrorMessage:(NSString *)errorMessage error:(NSError *)error { + return [self respondWithJSON:@{ @"error" : @{ @"message" : errorMessage } } error:error]; +} + +- (NSData *)respondWithServerErrorMessage:(NSString *)errorMessage { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:nil]; + return [self respondWithServerErrorMessage:errorMessage error:error]; +} + +- (NSData *)respondWithJSON:(NSDictionary *)JSON error:(NSError *)error { + NSError *JSONEncodingError; + NSData *data; + if (JSON) { + data = [NSJSONSerialization dataWithJSONObject:JSON + options:NSJSONWritingPrettyPrinted + error:&JSONEncodingError]; + } + NSAssert(!JSONEncodingError, @"An error occurred encoding the JSON response."); + [self respondWithData:data error:error]; + return data; +} + +- (NSData *)respondWithJSONError:(NSDictionary *)JSONError { + return [self respondWithJSON:JSONError + error:[NSError errorWithDomain:kFakeErrorDomain code:0 userInfo:nil]]; +} + +- (NSData *)respondWithError:(NSError *)error { + return [self respondWithJSON:nil error:error]; +} + +- (NSData *)respondWithJSON:(NSDictionary *)JSON { + return [self respondWithJSON:JSON error:nil]; +} + +@end diff --git a/Example/Auth/Tests/FIRGetAccountInfoRequestTests.m b/Example/Auth/Tests/FIRGetAccountInfoRequestTests.m new file mode 100644 index 0000000..6f713b0 --- /dev/null +++ b/Example/Auth/Tests/FIRGetAccountInfoRequestTests.m @@ -0,0 +1,87 @@ +/* + * Copyright 2017 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 "FIRAuthBackend.h" +#import "FIRGetOOBConfirmationCodeResponse.h" +#import "FIRGetAccountInfoRequest.h" +#import "FIRGetAccountInfoResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kIDTokenKey + @brief The key for the "idToken" value in the request. This is actually the STS Access Token, + despite it's confusing (backwards compatiable) parameter name. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** @var kTestAccessToken + @brief testing token. + */ +static NSString *const kTestAccessToken = @"testAccessToken"; + +/** @var kExpectedAPIURL + @brief The expected URL for test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=APIKey"; + +@interface FIRGetAccountInfoRequestTests : XCTestCase +@end +@implementation FIRGetAccountInfoRequestTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testGetAccountInfoRequest + @brief Tests the set account info request. + */ +- (void)testGetAccountInfoRequest { + FIRGetAccountInfoRequest *request = + [[FIRGetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey accessToken:kTestAccessToken]; + + [FIRAuthBackend getAccountInfo:request callback:^(FIRGetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + + }]; + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]); + XCTAssertNotNil(_RPCIssuer.decodedRequest[kIDTokenKey]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIDTokenKey], kTestAccessToken); +} + +@end diff --git a/Example/Auth/Tests/FIRGetAccountInfoResponseTests.m b/Example/Auth/Tests/FIRGetAccountInfoResponseTests.m new file mode 100644 index 0000000..b6c261e --- /dev/null +++ b/Example/Auth/Tests/FIRGetAccountInfoResponseTests.m @@ -0,0 +1,248 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthInternalErrors.h" +#import "FIRAuthBackend.h" +#import "FIRGetOOBConfirmationCodeResponse.h" +#import "FIRGetAccountInfoRequest.h" +#import "FIRGetAccountInfoResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kUsersKey + @brief the name of the "users" property in the response. + */ +static NSString *const kUsersKey = @"users"; + +/** @var kVerifiedProviderKey + @brief The name of the "VerifiedProvider" property in the response. + */ +static NSString *const kProviderUserInfoKey = @"providerUserInfo"; + +/** @var kPhotoUrlKey + @brief The name of the "photoURL" property in the response. + */ +static NSString *const kPhotoUrlKey = @"photoUrl"; + +/** @var kTestPhotoURL + @brief The fake photoUrl property value in the response. + */ +static NSString *const kTestPhotoURL = @"testPhotoURL"; + +/** @var kTestAccessToken + @brief testing token. + */ +static NSString *const kTestAccessToken = @"testAccessToken"; + +/** @var kProviderIDkey + @brief The name of the "provider ID" property in the response. + */ +static NSString *const kProviderIDkey = @"providerId"; + +/** @var kTestProviderID + @brief The fake providerID property value in the response. + */ +static NSString *const kTestProviderID = @"testProviderID"; + +/** @var kDisplayNameKey + @brief The name of the "Display Name" property in the response. + */ +static NSString *const kDisplayNameKey = @"displayName"; + +/** @var kTestDisplayName + @brief The fake DisplayName property value in the response. + */ +static NSString *const kTestDisplayName = @"DisplayName"; + +/** @var kFederatedIDKey + @brief The name of the "federated Id" property in the response. + */ +static NSString *const kFederatedIDKey = @"federatedId"; + +/** @var kTestFederatedID + @brief The fake federated Id property value in the response. + */ +static NSString *const kTestFederatedID = @"testFederatedId"; + +/** @var kEmailKey + @brief The name of the "Email" property in the response. + */ +static NSString *const kEmailKey = @"email"; + +/** @var kTestEmail + @brief The fake email property value in the response. + */ +static NSString *const kTestEmail = @"testEmail"; + +/** @var kPasswordHashKey + @brief The name of the "password hash" property in the response. + */ +static NSString *const kPasswordHashKey = @"passwordHash"; + +/** @var kTestPasswordHash + @brief The fake password hash property value in the response. + */ +static NSString *const kTestPasswordHash = @"testPasswordHash"; + +/** @var kLocalIDKey + @brief The key for the "localID" value in the response. + */ +static NSString *const kLocalIDKey = @"localId"; + +/** @var kTestLocalID + @brief The fake @c localID for testing in the response. + */ +static NSString *const kTestLocalID = @"testLocalId"; + +/** @var kEmailVerifiedKey + @brief The key for the "emailVerified" value in the response. + */ +static NSString *const kEmailVerifiedKey = @"emailVerified"; + +/** @class FIRGetAccountInfoResponseTests + @brief Tests for @c FIRGetAccountInfoResponse. + */ +@interface FIRGetAccountInfoResponseTests : XCTestCase +@end +@implementation FIRGetAccountInfoResponseTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testGetAccountInfoUnexpectedResponseError + @brief This test simulates an unexpected response returned from server in @c GetAccountInfo + flow. + */ +- (void)testGetAccountInfoUnexpectedResponseError { + FIRGetAccountInfoRequest *request = + [[FIRGetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey accessToken:kTestAccessToken]; + + __block BOOL callbackInvoked; + __block FIRGetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend getAccountInfo:request + callback:^(FIRGetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + NSArray *erroneousUserData = @[@"user1Data", @"user2Data"]; + [_RPCIssuer respondWithJSON:@{ + kUsersKey : erroneousUserData + }]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertEqualObjects(RPCError.domain, FIRAuthErrorDomain); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInternalError); + XCTAssertNotNil(RPCError.userInfo[NSUnderlyingErrorKey]); + NSError *underlyingError = RPCError.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlyingError); + XCTAssertNotNil(underlyingError.userInfo[FIRAuthErrorUserInfoDeserializedResponseKey]); + XCTAssertNil(RPCResponse); +} + +/** @fn testSuccessfulGetAccountInfoResponse + @brief This test simulates a successful @c GetAccountInfo flow. + */ +- (void)testSuccessfulGetAccountInfoResponse { + FIRGetAccountInfoRequest *request = + [[FIRGetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey accessToken:kTestAccessToken]; + + __block BOOL callbackInvoked; + __block FIRGetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend getAccountInfo:request + callback:^(FIRGetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + NSArray *users = @[ + @{ + kProviderUserInfoKey:@[ + @{ + kProviderIDkey : kTestProviderID, + kDisplayNameKey: kTestDisplayName, + kPhotoUrlKey : kTestPhotoURL, + kFederatedIDKey : kTestFederatedID, + kEmailKey : kTestEmail, + } + ], + kLocalIDKey : kTestLocalID, + kDisplayNameKey : kTestDisplayName, + kEmailKey : kTestEmail, + kPhotoUrlKey : kTestPhotoURL, + kEmailVerifiedKey : @YES, + kPasswordHashKey : kTestPasswordHash + } + ]; + [_RPCIssuer respondWithJSON:@{ + @"users" : users, + }]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + XCTAssertNotNil(RPCResponse.users); + if ([RPCResponse.users count]) { + NSURL *responsePhotoUrl = RPCResponse.users[0].photoURL; + XCTAssertEqualObjects(responsePhotoUrl.absoluteString, kTestPhotoURL); + XCTAssertEqualObjects(RPCResponse.users[0].displayName, kTestDisplayName); + XCTAssertEqualObjects(RPCResponse.users[0].email, kTestEmail); + XCTAssertEqualObjects(RPCResponse.users[0].localID, kTestLocalID); + XCTAssertEqual(RPCResponse.users[0].emailVerified, YES); + XCTAssertEqualObjects(RPCResponse.users[0].passwordHash, kTestPasswordHash); + NSArray<FIRGetAccountInfoResponseProviderUserInfo *> *providerUserInfo = + RPCResponse.users[0].providerUserInfo; + if ([providerUserInfo count]) { + NSURL *providerInfoPhotoUrl = providerUserInfo[0].photoURL; + XCTAssertEqualObjects(providerInfoPhotoUrl.absoluteString, kTestPhotoURL); + XCTAssertEqualObjects(providerUserInfo[0].providerID, kTestProviderID); + XCTAssertEqualObjects(providerUserInfo[0].displayName, kTestDisplayName); + XCTAssertEqualObjects(providerUserInfo[0].federatedID, kTestFederatedID); + XCTAssertEqualObjects(providerUserInfo[0].email, kTestEmail); + } + } +} + +@end diff --git a/Example/Auth/Tests/FIRGetOOBConfirmationCodeRequestTests.m b/Example/Auth/Tests/FIRGetOOBConfirmationCodeRequestTests.m new file mode 100644 index 0000000..d5a22aa --- /dev/null +++ b/Example/Auth/Tests/FIRGetOOBConfirmationCodeRequestTests.m @@ -0,0 +1,149 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRGetOOBConfirmationCodeRequest.h" +#import "FIRGetOOBConfirmationCodeResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kExpectedAPIURL + @brief The expected URL for the test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode?key=APIKey"; + +/** @var kRequestTypeKey + @brief The name of the required "requestType" property in the request. + */ +static NSString *const kRequestTypeKey = @"requestType"; + +/** @var kPasswordResetRequestTypeValue + @brief The value for the "PASSWORD_RESET" request type. + */ +static NSString *const kPasswordResetRequestTypeValue = @"PASSWORD_RESET"; + +/** @var kVerifyEmailRequestTypeValue + @brief The value for the "VERIFY_EMAIL" request type. + */ +static NSString *const kVerifyEmailRequestTypeValue = @"VERIFY_EMAIL"; + +/** @var kEmailKey + @brief The name of the "email" property in the request. + */ +static NSString *const kEmailKey = @"email"; + +/** @var kTestEmail + @brief Testing user email adadress. + */ +static NSString *const kTestEmail = @"test@gmail.com"; + +/** @var kAccessTokenKey + @brief The name of the "accessToken" property in the request. + */ +static NSString *const kAccessTokenKey = @"idToken"; + +/** @var kTestAccessToken + @brief Testing access token. + */ +static NSString *const kTestAccessToken = @"ACCESS_TOKEN"; + +/** @class FIRGetOOBConfirmationCodeRequestTests + @brief Tests for @c FIRGetOOBConfirmationCodeRequest. + */ +@interface FIRGetOOBConfirmationCodeRequestTests : XCTestCase +@end +@implementation FIRGetOOBConfirmationCodeRequestTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testPasswordResetRequest + @brief Tests the encoding of a password reset request. + */ +- (void)testPasswordResetRequest { + FIRGetOOBConfirmationCodeRequest *request = + [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail + APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRGetOOBConfirmationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend getOOBConfirmationCode:request + callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kEmailKey], kTestEmail); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kRequestTypeKey], kPasswordResetRequestTypeValue); +} + +/** @fn testEmailVerificationRequest + @brief Tests the encoding of an email verification request. + */ +- (void)testEmailVerificationRequest { + FIRGetOOBConfirmationCodeRequest *request = + [FIRGetOOBConfirmationCodeRequest verifyEmailRequestWithAccessToken:kTestAccessToken + APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRGetOOBConfirmationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend getOOBConfirmationCode:request + callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kAccessTokenKey], kTestAccessToken); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kRequestTypeKey], kVerifyEmailRequestTypeValue); +} + +@end diff --git a/Example/Auth/Tests/FIRGetOOBConfirmationCodeResponseTests.m b/Example/Auth/Tests/FIRGetOOBConfirmationCodeResponseTests.m new file mode 100644 index 0000000..98c9d8e --- /dev/null +++ b/Example/Auth/Tests/FIRGetOOBConfirmationCodeResponseTests.m @@ -0,0 +1,320 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRGetOOBConfirmationCodeRequest.h" +#import "FIRGetOOBConfirmationCodeResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestEmail + @brief Testing user email adadress. + */ +static NSString *const kTestEmail = @"test@gmail.com"; + +/** @var kTestAccessToken + @brief Testing access token. + */ +static NSString *const kTestAccessToken = @"ACCESS_TOKEN"; + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kOOBCodeKey + @brief The name of the field in the response JSON for the OOB code. + */ +static NSString *const kOOBCodeKey = @"oobCode"; + +/** @var kTestOOBCode + @brief Fake OOB Code used for testing. + */ +static NSString *const kTestOOBCode = @"OOBCode"; + +/** @var kEmailNotFoundMessage + @brief The value of the "message" field returned for an "email not found" error. + */ +static NSString *const kEmailNotFoundMessage = @"EMAIL_NOT_FOUND: fake custom message"; + +/** @var kInvalidEmailErrorMessage + @brief The error returned by the server if the email is invalid. + */ +static NSString *const kInvalidEmailErrorMessage = @"INVALID_EMAIL:"; + +/** @var kInvalidMessagePayloadErrorMessage + @brief This is the prefix for the error message the server responds with if an invalid message + payload was sent. + */ +static NSString *const kInvalidMessagePayloadErrorMessage = @"INVALID_MESSAGE_PAYLOAD"; + +/** @var kInvalidSenderErrorMessage + @brief This is the prefix for the error message the server responds with if invalid sender is + used to send the email for updating user's email address. + */ +static NSString *const kInvalidSenderErrorMessage = @"INVALID_SENDER"; + + +/** @var kInvalidRecipientEmailErrorMessage + @brief This is the prefix for the error message the server responds with if the recipient email + is invalid. + */ +static NSString *const kInvalidRecipientEmailErrorMessage = @"INVALID_RECIPIENT_EMAIL"; + +/** @class FIRGetOOBConfirmationCodeResponseTests + @brief Tests for @c FIRGetOOBConfirmationCodeResponse. + */ +@interface FIRGetOOBConfirmationCodeResponseTests : XCTestCase +@end +@implementation FIRGetOOBConfirmationCodeResponseTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testSuccessfulPasswordResetResponse + @brief This test simulates a complete password reset response (with OOB Code) and makes sure + it succeeds, and we get the OOB Code decoded correctly. + */ +- (void)testSuccessfulPasswordResetResponse { + FIRGetOOBConfirmationCodeRequest *request = + [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail + APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRGetOOBConfirmationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend getOOBConfirmationCode:request + callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + kOOBCodeKey : kTestOOBCode + }]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.OOBCode, kTestOOBCode); +} + +/** @fn testSuccessfulPasswordResetResponseWithoutOOBCode + @brief This test simulates a password reset request where we don't receive the optional OOBCode + response value. It should still succeed. + */ +- (void)testSuccessfulPasswordResetResponseWithoutOOBCode { + FIRGetOOBConfirmationCodeRequest *request = + [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail + APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRGetOOBConfirmationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend getOOBConfirmationCode:request + callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{}]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + XCTAssertNil(RPCResponse.OOBCode); +} + +/** @fn testEmailNotFoundError + @brief This test checks for email not found responses, and makes sure they are decoded to the + correct error response. + */ +- (void)testEmailNotFoundError { + FIRGetOOBConfirmationCodeRequest *request = + [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail + APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRGetOOBConfirmationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend getOOBConfirmationCode:request + callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kEmailNotFoundMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertEqualObjects(RPCError.domain, FIRAuthErrorDomain); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserNotFound); + XCTAssertNil(RPCResponse); +} + +/** @fn testInvalidEmailError + @brief This test checks for the INVALID_EMAIL error message from the backend. + */ +- (void)testInvalidEmailError { + FIRGetOOBConfirmationCodeRequest *request = + [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRGetOOBConfirmationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend getOOBConfirmationCode:request + callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kInvalidEmailErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertEqualObjects(RPCError.domain, FIRAuthErrorDomain); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidEmail); + XCTAssertNil(RPCResponse); +} + +/** @fn testInvalidMessagePayloadError + @brief Tests for @c FIRAuthErrorCodeInvalidMessagePayload. + */ +- (void)testInvalidMessagePayloadError { + FIRGetOOBConfirmationCodeRequest *request = + [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRGetOOBConfirmationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend getOOBConfirmationCode:request + callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kInvalidMessagePayloadErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidMessagePayload); +} + +/** @fn testInvalidSenderError + @brief Tests for @c FIRAuthErrorCodeInvalidSender. + */ +- (void)testInvalidSenderError { + FIRGetOOBConfirmationCodeRequest *request = + [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail + APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRGetOOBConfirmationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend getOOBConfirmationCode:request + callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + [_RPCIssuer respondWithServerErrorMessage:kInvalidSenderErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidSender); +} + +/** @fn testInvalidRecipientEmailError + @brief Tests for @c FIRAuthErrorCodeInvalidRecipientEmail. + */ +- (void)testInvalidRecipientEmailError { + FIRGetOOBConfirmationCodeRequest *request = + [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail + APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRGetOOBConfirmationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend getOOBConfirmationCode:request + callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + [_RPCIssuer respondWithServerErrorMessage:kInvalidRecipientEmailErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidRecipientEmail); +} + +/** @fn testSuccessfulEmailVerificationResponse + @brief This test is really not much different than the original test for password reset. But + it's here for completeness sake. + */ +- (void)testSuccessfulEmailVerificationResponse { + FIRGetOOBConfirmationCodeRequest *request = + [FIRGetOOBConfirmationCodeRequest passwordResetRequestWithEmail:kTestEmail + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRGetOOBConfirmationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend getOOBConfirmationCode:request + callback:^(FIRGetOOBConfirmationCodeResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + kOOBCodeKey : kTestOOBCode + }]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.OOBCode, kTestOOBCode); +} + +@end diff --git a/Example/Auth/Tests/FIRGitHubAuthProviderTests.m b/Example/Auth/Tests/FIRGitHubAuthProviderTests.m new file mode 100644 index 0000000..dfcd87f --- /dev/null +++ b/Example/Auth/Tests/FIRGitHubAuthProviderTests.m @@ -0,0 +1,52 @@ +/* + * Copyright 2017 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 "GitHub/FIRGitHubAuthProvider.h" +#import "FIRAuthCredential_Internal.h" +#import "FIRVerifyAssertionRequest.h" + +/** @var kGitHubToken + @brief A testing GitHub token. + */ +static NSString *const kGitHubToken = @"Token"; + +/** @var kAPIKey + @brief A testing API Key. + */ +static NSString *const kAPIKey = @"APIKey"; + +/** @class FIRGitHubAuthProviderTests + @brief Tests for @c FIRGitHubAuthProvider + */ +@interface FIRGitHubAuthProviderTests : XCTestCase +@end +@implementation FIRGitHubAuthProviderTests + +/** @fn testCredentialWithToken + @brief Tests the @c credentialWithToken method to make sure the credential it produces populates + the appropriate fields in a verify assertion request. + */ +- (void)testCredentialWithToken { + FIRAuthCredential *credential = [FIRGitHubAuthProvider credentialWithToken:kGitHubToken]; + FIRVerifyAssertionRequest *request = + [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kAPIKey providerID:FIRGitHubAuthProviderID]; + [credential prepareVerifyAssertionRequest:request]; + XCTAssertEqualObjects(request.providerAccessToken, kGitHubToken); +} + +@end diff --git a/Example/Auth/Tests/FIRPhoneAuthProviderTests.m b/Example/Auth/Tests/FIRPhoneAuthProviderTests.m new file mode 100644 index 0000000..f907601 --- /dev/null +++ b/Example/Auth/Tests/FIRPhoneAuthProviderTests.m @@ -0,0 +1,550 @@ +/* + * Copyright 2017 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 "Phone/FIRPhoneAuthProvider.h" +#import "Phone/FIRPhoneAuthCredential_Internal.h" +#import "Phone/NSString+FIRAuth.h" +#import "FIRAuthAPNSToken.h" +#import "FIRAuthAPNSTokenManager.h" +#import "FIRAuthAppCredential.h" +#import "FIRAuthAppCredentialManager.h" +#import "FIRAuthNotificationManager.h" +#import "FIRAuth_Internal.h" +#import "FIRAuthCredential_Internal.h" +#import "FIRAuthErrorUtils.h" +#import "FIRAuthGlobalWorkQueue.h" +#import "FIRAuthBackend.h" +#import "FIRSendVerificationCodeRequest.h" +#import "FIRSendVerificationCodeResponse.h" +#import "FIRVerifyClientRequest.h" +#import "FIRVerifyClientResponse.h" +#import "FIRApp+FIRAuthUnitTests.h" +#import "OCMStubRecorder+FIRAuthUnitTests.h" +#import <OCMock/OCMock.h> + +NS_ASSUME_NONNULL_BEGIN + +/** @var kTestPhoneNumber + @brief A testing phone number. + */ +static NSString *const kTestPhoneNumber = @"55555555"; + +/** @var kTestInvalidPhoneNumber + @brief An invalid testing phone number. + */ +static NSString *const kTestInvalidPhoneNumber = @"555+!*55555"; + +/** @var kTestVerificationID + @brief A testing verfication ID. + */ +static NSString *const kTestVerificationID = @"verificationID"; + +/** @var kTestReceipt + @brief A fake receipt for testing. + */ +static NSString *const kTestReceipt = @"receipt"; + +/** @var kTestSecret + @brief A fake secret for testing. + */ +static NSString *const kTestSecret = @"secret"; + +/** @var kTestOldReceipt + @brief A fake old receipt for testing. + */ +static NSString *const kTestOldReceipt = @"old_receipt"; + +/** @var kTestOldSecret + @brief A fake old secret for testing. + */ +static NSString *const kTestOldSecret = @"old_secret"; + + +/** @var kTestVerificationCode + @brief A fake verfication code. + */ +static NSString *const kTestVerificationCode = @"verificationCode"; + +/** @var kTestTimeout + @brief A fake timeout value for waiting for push notification. + */ +static const NSTimeInterval kTestTimeout = 5; + +/** @var kAPIKey + @brief The fake API key. + */ +static NSString *const kAPIKey = @"FAKE_API_KEY"; + +/** @var kExpectationTimeout + @brief The maximum time waiting for expectations to fulfill. + */ +static const NSTimeInterval kExpectationTimeout = 1; + +/** @class FIRPhoneAuthProviderTests + @brief Tests for @c FIRPhoneAuthProvider + */ +@interface FIRPhoneAuthProviderTests : XCTestCase +@end + +@implementation FIRPhoneAuthProviderTests { + /** @var _mockBackend + @brief The mock @c FIRAuthBackendImplementation . + */ + id _mockBackend; + + /** @var _provider + @brief The @c FIRPhoneAuthProvider instance under test. + */ + FIRPhoneAuthProvider *_provider; + + /** @var _mockAuth + @brief The mock @c FIRAuth instance associated with @c _provider . + */ + id _mockAuth; + + /** @var _mockAPNSTokenManager + @brief The mock @c FIRAuthAPNSTokenManager instance associated with @c _mockAuth . + */ + id _mockAPNSTokenManager; + + /** @var _mockAppCredentialManager + @brief The mock @c FIRAuthAppCredentialManager instance associated with @c _mockAuth . + */ + id _mockAppCredentialManager; + + /** @var _mockNotificationManager + @brief The mock @c FIRAuthNotificationManager instance associated with @c _mockAuth . + */ + id _mockNotificationManager; +} + +- (void)setUp { + [super setUp]; + _mockBackend = OCMProtocolMock(@protocol(FIRAuthBackendImplementation)); + [FIRAuthBackend setBackendImplementation:_mockBackend]; + _mockAuth = OCMClassMock([FIRAuth class]); + _mockAPNSTokenManager = OCMClassMock([FIRAuthAPNSTokenManager class]); + OCMStub([_mockAuth tokenManager]).andReturn(_mockAPNSTokenManager); + _mockAppCredentialManager = OCMClassMock([FIRAuthAppCredentialManager class]); + OCMStub([_mockAuth appCredentialManager]).andReturn(_mockAppCredentialManager); + _mockNotificationManager = OCMClassMock([FIRAuthNotificationManager class]); + OCMStub([_mockAuth notificationManager]).andReturn(_mockNotificationManager); + _provider = [FIRPhoneAuthProvider providerWithAuth:_mockAuth]; +} + +- (void)tearDown { + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testCredentialWithVerificationID + @brief Tests the @c credentialWithToken method to make sure that it returns a valid + FIRAuthCredential instance. + */ +- (void)testCredentialWithVerificationID { + FIRPhoneAuthCredential *credential = + [_provider credentialWithVerificationID:kTestVerificationID + verificationCode:kTestVerificationCode]; + XCTAssertEqualObjects(credential.verificationID, kTestVerificationID); + XCTAssertEqualObjects(credential.verificationCode, kTestVerificationCode); + XCTAssertNil(credential.temporaryProof); + XCTAssertNil(credential.phoneNumber); +} + +/** @fn testVerifyEmptyPhoneNumber + @brief Tests a failed invocation @c verifyPhoneNumber:completion: because an empty phone + number was provided. + */ +- (void)testVerifyEmptyPhoneNumber { + // Empty phone number is checked on the client side so no backend RPC is mocked. + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider verifyPhoneNumber:@"" + completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqual(error.code, FIRAuthErrorCodeMissingPhoneNumber); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + +/** @fn testVerifyInvalidPhoneNumber + @brief Tests a failed invocation @c verifyPhoneNumber:completion: because an invalid phone + number was provided. + */ +- (void)testVerifyInvalidPhoneNumber { + OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY]) + .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(YES); }); + OCMStub([_mockAppCredentialManager credential]) + .andReturn([[FIRAuthAppCredential alloc] initWithReceipt:kTestReceipt secret:kTestSecret]); + OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSendVerificationCodeRequest *request, + FIRSendVerificationCodeResponseCallback callback) { + XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber); + XCTAssertEqualObjects(request.appCredential.receipt, kTestReceipt); + XCTAssertEqualObjects(request.appCredential.secret, kTestSecret); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback(nil, [FIRAuthErrorUtils invalidPhoneNumberErrorWithMessage:nil]); + }); + }); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider verifyPhoneNumber:kTestPhoneNumber + completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(verificationID); + XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidPhoneNumber); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + OCMVerifyAll(_mockNotificationManager); + OCMVerifyAll(_mockAppCredentialManager); +} + +/** @fn testVerifyPhoneNumber + @brief Tests a successful invocation of @c verifyPhoneNumber:completion:. + */ +- (void)testVerifyPhoneNumber { + OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY]) + .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(YES); }); + OCMStub([_mockAppCredentialManager credential]) + .andReturn([[FIRAuthAppCredential alloc] initWithReceipt:kTestReceipt secret:kTestSecret]); + OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSendVerificationCodeRequest *request, + FIRSendVerificationCodeResponseCallback callback) { + XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber); + XCTAssertEqualObjects(request.appCredential.receipt, kTestReceipt); + XCTAssertEqualObjects(request.appCredential.secret, kTestSecret); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSendVerificationCodeResponse = OCMClassMock([FIRSendVerificationCodeResponse class]); + OCMStub([mockSendVerificationCodeResponse verificationID]).andReturn(kTestVerificationID); + callback(mockSendVerificationCodeResponse, nil); + }); + }); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider verifyPhoneNumber:kTestPhoneNumber + completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(error); + XCTAssertEqualObjects(verificationID, kTestVerificationID); + XCTAssertEqualObjects(verificationID.fir_authPhoneNumber, kTestPhoneNumber); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + OCMVerifyAll(_mockNotificationManager); + OCMVerifyAll(_mockAppCredentialManager); +} + +/** @fn testNotForwardingNotification + @brief Tests returning an error for the app failing to forward notification. + */ +- (void)testNotForwardingNotification { + OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY]) + .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(NO); }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider verifyPhoneNumber:kTestPhoneNumber + completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(verificationID); + XCTAssertEqual(error.code, FIRAuthErrorCodeNotificationNotForwarded); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockNotificationManager); +} + +/** @fn testMissingAPNSToken + @brief Tests returning an error for the app failing to provide an APNS device token. + */ +- (void)testMissingAPNSToken { + OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY]) + .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(YES); }); + OCMExpect([_mockAppCredentialManager credential]).andReturn(nil); + OCMExpect([_mockAPNSTokenManager getTokenWithCallback:OCMOCK_ANY]) + .andCallBlock1(^(FIRAuthAPNSTokenCallback callback) { callback(nil); }); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider verifyPhoneNumber:kTestPhoneNumber + completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(verificationID); + XCTAssertEqual(error.code, FIRAuthErrorCodeMissingAppToken); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockNotificationManager); + OCMVerifyAll(_mockAppCredentialManager); + OCMVerifyAll(_mockAPNSTokenManager); +} + +/** @fn testVerifyClient + @brief Tests verifying client before sending verification code. + */ +- (void)testVerifyClient { + OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY]) + .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(YES); }); + OCMExpect([_mockAppCredentialManager credential]).andReturn(nil); + NSData *data = [@"!@#$%^" dataUsingEncoding:NSUTF8StringEncoding]; + FIRAuthAPNSToken *token = [[FIRAuthAPNSToken alloc] initWithData:data + type:FIRAuthAPNSTokenTypeProd]; + OCMExpect([_mockAPNSTokenManager getTokenWithCallback:OCMOCK_ANY]) + .andCallBlock1(^(FIRAuthAPNSTokenCallback callback) { callback(token); }); + // Expect verify client request to the backend. + OCMExpect([_mockBackend verifyClient:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyClientRequest *request, + FIRVerifyClientResponseCallback callback) { + XCTAssertEqualObjects(request.appToken, @"21402324255E"); + XCTAssertFalse(request.isSandbox); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVerifyClientResponse = OCMClassMock([FIRVerifyClientResponse class]); + OCMStub([mockVerifyClientResponse receipt]).andReturn(kTestReceipt); + OCMStub([mockVerifyClientResponse suggestedTimeOutDate]) + .andReturn([NSDate dateWithTimeIntervalSinceNow:kTestTimeout]); + callback(mockVerifyClientResponse, nil); + }); + }); + // Mock receiving of push notification. + OCMExpect([[_mockAppCredentialManager ignoringNonObjectArgs] + didStartVerificationWithReceipt:OCMOCK_ANY timeout:0 callback:OCMOCK_ANY]) + .andCallIdDoubleIdBlock(^(NSString *receipt, + NSTimeInterval timeout, + FIRAuthAppCredentialCallback callback) { + XCTAssertEqualObjects(receipt, kTestReceipt); + // Unfortunately 'ignoringNonObjectArgs' means the real value for 'timeout' doesn't get passed + // into the block either, so we can't verify it here. + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback([[FIRAuthAppCredential alloc] initWithReceipt:kTestReceipt secret:kTestSecret]); + }); + }); + // Expect send verification code request to the backend. + OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSendVerificationCodeRequest *request, + FIRSendVerificationCodeResponseCallback callback) { + XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber); + XCTAssertEqualObjects(request.appCredential.receipt, kTestReceipt); + XCTAssertEqualObjects(request.appCredential.secret, kTestSecret); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSendVerificationCodeResponse = OCMClassMock([FIRSendVerificationCodeResponse class]); + OCMStub([mockSendVerificationCodeResponse verificationID]).andReturn(kTestVerificationID); + callback(mockSendVerificationCodeResponse, nil); + }); + }); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider verifyPhoneNumber:kTestPhoneNumber + completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(error); + XCTAssertEqualObjects(verificationID, kTestVerificationID); + XCTAssertEqualObjects(verificationID.fir_authPhoneNumber, kTestPhoneNumber); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + OCMVerifyAll(_mockNotificationManager); + OCMVerifyAll(_mockAppCredentialManager); + OCMVerifyAll(_mockAPNSTokenManager); +} + +/** @fn testSendVerificationCodeFailedRetry + @brief Tests failed retry after failing to send verification code. + */ +- (void)testSendVerificationCodeFailedRetry { + OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY]) + .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(YES); }); + + // Expect twice due to null check consumes one expectation. + OCMExpect([_mockAppCredentialManager credential]) + .andReturn([[FIRAuthAppCredential alloc] initWithReceipt:kTestOldReceipt + secret:kTestOldSecret]); + OCMExpect([_mockAppCredentialManager credential]) + .andReturn([[FIRAuthAppCredential alloc] initWithReceipt:kTestOldReceipt + secret:kTestOldSecret]); + NSData *data = [@"!@#$%^" dataUsingEncoding:NSUTF8StringEncoding]; + FIRAuthAPNSToken *token = [[FIRAuthAPNSToken alloc] initWithData:data + type:FIRAuthAPNSTokenTypeProd]; + + // Expect first sendVerificationCode request to the backend, with request containing old app + // credential. + OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSendVerificationCodeRequest *request, + FIRSendVerificationCodeResponseCallback callback) { + XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber); + XCTAssertEqualObjects(request.appCredential.receipt, kTestOldReceipt); + XCTAssertEqualObjects(request.appCredential.secret, kTestOldSecret); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback(nil, [FIRAuthErrorUtils invalidAppCredentialWithMessage:nil]); + }); + }); + + // Expect send verification code request to the backend, with request containing new app + // credential data. + OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSendVerificationCodeRequest *request, + FIRSendVerificationCodeResponseCallback callback) { + XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber); + XCTAssertEqualObjects(request.appCredential.receipt, kTestReceipt); + XCTAssertEqualObjects(request.appCredential.secret, kTestSecret); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback(nil, [FIRAuthErrorUtils invalidAppCredentialWithMessage:nil]); + }); + }); + + OCMExpect([_mockAPNSTokenManager getTokenWithCallback:OCMOCK_ANY]) + .andCallBlock1(^(FIRAuthAPNSTokenCallback callback) { callback(token); }); + // Expect verify client request to the backend. + OCMExpect([_mockBackend verifyClient:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyClientRequest *request, + FIRVerifyClientResponseCallback callback) { + XCTAssertEqualObjects(request.appToken, @"21402324255E"); + XCTAssertFalse(request.isSandbox); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVerifyClientResponse = OCMClassMock([FIRVerifyClientResponse class]); + OCMStub([mockVerifyClientResponse receipt]).andReturn(kTestReceipt); + OCMStub([mockVerifyClientResponse suggestedTimeOutDate]) + .andReturn([NSDate dateWithTimeIntervalSinceNow:kTestTimeout]); + callback(mockVerifyClientResponse, nil); + }); + }); + + // Mock receiving of push notification. + OCMStub([[_mockAppCredentialManager ignoringNonObjectArgs] + didStartVerificationWithReceipt:OCMOCK_ANY timeout:0 callback:OCMOCK_ANY]) + .andCallIdDoubleIdBlock(^(NSString *receipt, + NSTimeInterval timeout, + FIRAuthAppCredentialCallback callback) { + XCTAssertEqualObjects(receipt, kTestReceipt); + // Unfortunately 'ignoringNonObjectArgs' means the real value for 'timeout' doesn't get passed + // into the block either, so we can't verify it here. + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback([[FIRAuthAppCredential alloc] initWithReceipt:kTestReceipt secret:kTestSecret]); + }); + }); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider verifyPhoneNumber:kTestPhoneNumber + completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(verificationID); + XCTAssertEqual(error.code, FIRAuthErrorCodeInternalError); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + OCMVerifyAll(_mockNotificationManager); + OCMVerifyAll(_mockAppCredentialManager); + OCMVerifyAll(_mockAPNSTokenManager); +} + +/** @fn testSendVerificationCodeSuccessFulRetry + @brief Tests successful retry after failing to send verification code. + */ +- (void)testSendVerificationCodeSuccessFulRetry { + OCMExpect([_mockNotificationManager checkNotificationForwardingWithCallback:OCMOCK_ANY]) + .andCallBlock1(^(FIRAuthNotificationForwardingCallback callback) { callback(YES); }); + + // Expect twice due to null check consumes one expectation. + OCMExpect([_mockAppCredentialManager credential]) + .andReturn([[FIRAuthAppCredential alloc] initWithReceipt:kTestOldReceipt + secret:kTestOldSecret]); + OCMExpect([_mockAppCredentialManager credential]) + .andReturn([[FIRAuthAppCredential alloc] initWithReceipt:kTestOldReceipt + secret:kTestOldSecret]); + NSData *data = [@"!@#$%^" dataUsingEncoding:NSUTF8StringEncoding]; + FIRAuthAPNSToken *token = [[FIRAuthAPNSToken alloc] initWithData:data + type:FIRAuthAPNSTokenTypeProd]; + + // Expect first sendVerificationCode request to the backend, with request containing old app + // credential. + OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSendVerificationCodeRequest *request, + FIRSendVerificationCodeResponseCallback callback) { + XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber); + XCTAssertEqualObjects(request.appCredential.receipt, kTestOldReceipt); + XCTAssertEqualObjects(request.appCredential.secret, kTestOldSecret); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback(nil, [FIRAuthErrorUtils invalidAppCredentialWithMessage:nil]); + }); + }); + + // Expect send verification code request to the backend, with request containing new app + // credential data. + OCMExpect([_mockBackend sendVerificationCode:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSendVerificationCodeRequest *request, + FIRSendVerificationCodeResponseCallback callback) { + XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber); + XCTAssertEqualObjects(request.appCredential.receipt, kTestReceipt); + XCTAssertEqualObjects(request.appCredential.secret, kTestSecret); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSendVerificationCodeResponse = OCMClassMock([FIRSendVerificationCodeResponse class]); + OCMStub([mockSendVerificationCodeResponse verificationID]).andReturn(kTestVerificationID); + callback(mockSendVerificationCodeResponse, nil); + }); + }); + + OCMExpect([_mockAPNSTokenManager getTokenWithCallback:OCMOCK_ANY]) + .andCallBlock1(^(FIRAuthAPNSTokenCallback callback) { callback(token); }); + // Expect verify client request to the backend. + OCMExpect([_mockBackend verifyClient:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyClientRequest *request, + FIRVerifyClientResponseCallback callback) { + XCTAssertEqualObjects(request.appToken, @"21402324255E"); + XCTAssertFalse(request.isSandbox); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVerifyClientResponse = OCMClassMock([FIRVerifyClientResponse class]); + OCMStub([mockVerifyClientResponse receipt]).andReturn(kTestReceipt); + OCMStub([mockVerifyClientResponse suggestedTimeOutDate]) + .andReturn([NSDate dateWithTimeIntervalSinceNow:kTestTimeout]); + callback(mockVerifyClientResponse, nil); + }); + }); + + // Mock receiving of push notification. + OCMStub([[_mockAppCredentialManager ignoringNonObjectArgs] + didStartVerificationWithReceipt:OCMOCK_ANY timeout:0 callback:OCMOCK_ANY]) + .andCallIdDoubleIdBlock(^(NSString *receipt, + NSTimeInterval timeout, + FIRAuthAppCredentialCallback callback) { + XCTAssertEqualObjects(receipt, kTestReceipt); + // Unfortunately 'ignoringNonObjectArgs' means the real value for 'timeout' doesn't get passed + // into the block either, so we can't verify it here. + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback([[FIRAuthAppCredential alloc] initWithReceipt:kTestReceipt secret:kTestSecret]); + }); + }); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider verifyPhoneNumber:kTestPhoneNumber + completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects(verificationID, kTestVerificationID); + XCTAssertEqualObjects(verificationID.fir_authPhoneNumber, kTestPhoneNumber); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); + OCMVerifyAll(_mockNotificationManager); + OCMVerifyAll(_mockAppCredentialManager); + OCMVerifyAll(_mockAPNSTokenManager); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Auth/Tests/FIRResetPasswordRequestTests.m b/Example/Auth/Tests/FIRResetPasswordRequestTests.m new file mode 100644 index 0000000..d0ccc5d --- /dev/null +++ b/Example/Auth/Tests/FIRResetPasswordRequestTests.m @@ -0,0 +1,101 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRResetPasswordRequest.h" +#import "FIRResetPasswordResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kTestOOBCode + @brief Fake OOBCode used for testing. + */ +static NSString *const kTestOOBCode = @"OOBCode"; + +/** @var kTestNewPassword + @brief Fake new password used for testing. + */ +static NSString *const kTestNewPassword = @"newPassword:-)"; + +/** @var kOOBCodeKey + @brief The "resetPassword" key. + */ +static NSString *const kOOBCodeKey = @"oobCode"; + +/** @var knewPasswordKey + @brief The "newPassword" key. + */ +static NSString *const knewPasswordKey = @"newPassword"; + +/** @var kExpectedAPIURL + @brief The expected URL for test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPassword?key=APIKey"; + +/** @class FIRResetPasswordRequestTests + @brief Tests for @c FIRResetPasswordRequest. + */ +@interface FIRResetPasswordRequestTest : XCTestCase +@end + +@implementation FIRResetPasswordRequestTest { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testResetPasswordRequest + @brief Tests the reset password reqeust. + */ +- (void)testResetPasswordRequest { + FIRResetPasswordRequest *request = + [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey + oobCode:kTestOOBCode + newPassword:kTestNewPassword]; + [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response, + NSError *_Nullable error) { + + }]; + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[knewPasswordKey], kTestNewPassword); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kOOBCodeKey], kTestOOBCode); +} + +@end diff --git a/Example/Auth/Tests/FIRResetPasswordResponseTests.m b/Example/Auth/Tests/FIRResetPasswordResponseTests.m new file mode 100644 index 0000000..51f1155 --- /dev/null +++ b/Example/Auth/Tests/FIRResetPasswordResponseTests.m @@ -0,0 +1,257 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRResetPasswordRequest.h" +#import "FIRResetPasswordResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kUserDisabledErrorMessage + @brief This is the error message the server will respond with if the user's account has been + disabled. + */ +static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED"; + +/** @var kOperationNotAllowedErrorMessage + @brief This is the error message the server will respond with if Admin disables IDP specified by + provider. + */ +static NSString *const kOperationNotAllowedErrorMessage = @"OPERATION_NOT_ALLOWED"; + +/** @var kExpiredActionCodeErrorMessage + @brief This is the error message the server will respond with if the action code is expired. + */ +static NSString *const kExpiredActionCodeErrorMessage = @"EXPIRED_OOB_CODE"; + +/** @var kInvalidActionCodeErrorMessage + @brief This is the error message the server will respond with if the action code is invalid. + */ +static NSString *const kInvalidActionCodeErrorMessage = @"INVALID_OOB_CODE"; + +/** @var kWeakPasswordErrorMessagePrefix + @brief This is the prefix for the error message the server responds with if user's new password + to be set is too weak. + */ +static NSString *const kWeakPasswordErrorMessagePrefix = @"WEAK_PASSWORD : "; + +/** @var kTestOOBCode + @brief Fake OOBCode used for testing. + */ +static NSString *const kTestOOBCode = @"OOBCode"; + +/** @var kTestNewPassword + @brief Fake new password used for testing. + */ +static NSString *const kTestNewPassword = @"newPassword"; + +/** @var kEmailKey + @brief The key for the email returned in the response. + */ +static NSString *const kEmailKey = @"email"; + +/** @var kRequestTypeKey + @brief The key for the request type returned in the response. + */ +static NSString *const kRequestTypeKey = @"requestType"; + +/** @var kTestEmail + @brief The email returned in the response. + */ +static NSString *const kTestEmail = @"test@email.com"; + +/** @var kResetPasswordExpectedRequestType. + @brief The expected request type returned for reset password request. + */ +static NSString *const kExpectedResetPasswordRequestType = @"PASSWORD_RESET"; + +/** @class FIRResetPasswordRequestTests + @brief Tests for @c FIRResetPasswordRequest. + */ +@interface FIRResetPasswordResponseTests : XCTestCase +@end + +@implementation FIRResetPasswordResponseTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testUserDisabledError + @brief Tests for @c FIRAuthErrorCodeUserDisabled. + */ +- (void)testUserDisabledError { + FIRResetPasswordRequest *request = + [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey + oobCode:kTestOOBCode + newPassword:kTestNewPassword]; + __block BOOL callbackInvoked; + __block FIRResetPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kUserDisabledErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserDisabled); +} + +/** @fn testOperationNotAllowedError + @brief Tests for @c FIRAuthErrorCodeOperationNotAllowed. + */ +- (void)testOperationNotAllowedError { + FIRResetPasswordRequest *request = + [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey + oobCode:kTestOOBCode + newPassword:kTestNewPassword]; + __block BOOL callbackInvoked; + __block FIRResetPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kOperationNotAllowedErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed); +} + +/** @fn testOOBExpiredError + @brief Tests for @c FIRAuthErrorCodeExpiredActionCode. + */ +- (void)testOOBExpiredError { + FIRResetPasswordRequest *request = + [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey + oobCode:kTestOOBCode + newPassword:kTestNewPassword]; + __block BOOL callbackInvoked; + __block FIRResetPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kExpiredActionCodeErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeExpiredActionCode); +} + +/** @fn testOOBInvalidError + @brief Tests for @c FIRAuthErrorCodeInvalidActionCode. + */ +- (void)testOOBInvalidError { + FIRResetPasswordRequest *request = + [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey + oobCode:kTestOOBCode + newPassword:kTestNewPassword]; + __block BOOL callbackInvoked; + __block FIRResetPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kInvalidActionCodeErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidActionCode); +} + +/** @fn testWeakPasswordError + @brief Tests for @c FIRAuthErrorCodeWeakPassword. + */ +- (void)testWeakPasswordError { + FIRResetPasswordRequest *request = + [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey + oobCode:kTestOOBCode + newPassword:kTestNewPassword]; + __block BOOL callbackInvoked; + __block FIRResetPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kWeakPasswordErrorMessagePrefix]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeWeakPassword); +} + +/** @fn testSuccessfulResetPassword + @brief Tests a successful reset password flow. + */ +- (void)testSuccessfulResetPassword { + FIRResetPasswordRequest *request = + [[FIRResetPasswordRequest alloc] initWithAPIKey:kTestAPIKey + oobCode:kTestOOBCode + newPassword:kTestNewPassword]; + __block BOOL callbackInvoked; + __block FIRResetPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend resetPassword:request callback:^(FIRResetPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithJSON:@{ + kEmailKey : kTestEmail, + kRequestTypeKey : kExpectedResetPasswordRequestType + }]; + XCTAssert(callbackInvoked); + XCTAssertEqualObjects(RPCResponse.email, kTestEmail); + XCTAssertEqualObjects(RPCResponse.requestType, kExpectedResetPasswordRequestType); + XCTAssertNil(RPCError); +} + +@end diff --git a/Example/Auth/Tests/FIRSendVerificationCodeRequestTests.m b/Example/Auth/Tests/FIRSendVerificationCodeRequestTests.m new file mode 100644 index 0000000..5582d32 --- /dev/null +++ b/Example/Auth/Tests/FIRSendVerificationCodeRequestTests.m @@ -0,0 +1,119 @@ +/* + * Copyright 2017 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 "FIRAuthAppCredential.h" +#import "FIRAuthBackend.h" +#import "FIRSendVerificationCodeRequest.h" +#import "FIRSendVerificationCodeResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kTestPhoneNumber + @brief Fake phone number used for testing. + */ +static NSString *const kTestPhoneNumber = @"12345678"; + +/** @var kTestSecret + @brief Fake secret used for testing. + */ +static NSString *const kTestSecret = @"secret"; + +/** @var kTestReceipt + @brief Fake receipt used for testing. + */ +static NSString *const kTestReceipt = @"receipt"; + +/** @var kPhoneNumberKey + @brief The key for the "phone number" value in the request. + */ +static NSString *const kPhoneNumberKey = @"phoneNumber"; + +/** @var kReceiptKey + @brief The key for the receipt parameter in the request. + */ +static NSString *const kReceiptKey = @"iosReceipt"; + +/** @var kSecretKey + @brief The key for the Secret parameter in the request. + */ +static NSString *const kSecretKey = @"iosSecret"; + +/** @var kExpectedAPIURL + @brief The expected URL for the test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/sendVerificationCode?key=APIKey"; + +/** @class FIRSendVerificationCodeRequestTests + @brief Tests for @c FIRSendVerificationCodeRequest. + */ +@interface FIRSendVerificationCodeRequestTests : XCTestCase +@end + +@implementation FIRSendVerificationCodeRequestTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testSendVerificationCodeRequest + @brief Tests the sendVerificationCode request. + */ +- (void)testSendVerificationCodeRequest { + FIRAuthAppCredential *credential = + [[FIRAuthAppCredential alloc]initWithReceipt:kTestReceipt secret:kTestSecret]; + FIRSendVerificationCodeRequest *request = + [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:kTestPhoneNumber + appCredential:credential + APIKey:kTestAPIKey]; + XCTAssertEqualObjects(request.phoneNumber, kTestPhoneNumber); + XCTAssertEqualObjects(request.appCredential.receipt, kTestReceipt); + XCTAssertEqualObjects(request.appCredential.secret, kTestSecret); + + [FIRAuthBackend sendVerificationCode:request + callback:^(FIRSendVerificationCodeResponse *_Nullable response, + NSError *_Nullable error) { + }]; + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPhoneNumberKey], kTestPhoneNumber); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPhoneNumberKey], kTestPhoneNumber); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kReceiptKey], kTestReceipt); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kSecretKey], kTestSecret); +} + +@end diff --git a/Example/Auth/Tests/FIRSendVerificationCodeResponseTests.m b/Example/Auth/Tests/FIRSendVerificationCodeResponseTests.m new file mode 100644 index 0000000..5a1244b --- /dev/null +++ b/Example/Auth/Tests/FIRSendVerificationCodeResponseTests.m @@ -0,0 +1,221 @@ +/* + * Copyright 2017 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 "FIRAuthAppCredential.h" +#import "FIRAuthErrors.h" +#import "FIRAuthErrorUtils.h" +#import "FIRAuthBackend.h" +#import "FIRSendVerificationCodeRequest.h" +#import "FIRSendVerificationCodeResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kTestPhoneNumber + @brief Fake phone number used for testing. + */ +static NSString *const kTestPhoneNumber = @"12345678"; + +/** @var kTestInvalidPhoneNumber + @brief An invalid testing phone number. + */ +static NSString *const kTestInvalidPhoneNumber = @"555+!*55555"; + +/** @var kVerificationIDKey + @brief Fake key for the test verification ID. + */ +static NSString *const kVerificationIDKey = @"sessionInfo"; + +/** @var kFakeVerificationID + @brief Fake verification ID for testing. + */ +static NSString *const kFakeVerificationID = @"testVerificationID"; + +/** @var kTestSecret + @brief Fake secret used for testing. + */ +static NSString *const kTestSecret = @"secret"; + +/** @var kTestReceipt + @brief Fake receipt used for testing. + */ +static NSString *const kTestReceipt = @"receipt"; + +/** @var kInvalidPhoneNumberErrorMessage + @brief This is the error message the server will respond with if an incorrectly formatted phone + number is provided. + */ +static NSString *const kInvalidPhoneNumberErrorMessage = @"INVALID_PHONE_NUMBER"; + +/** @var kQuotaExceededErrorMessage + @brief This is the error message the server will respond with if the quota for SMS text messages + has been exceeded for the project. + */ +static NSString *const kQuotaExceededErrorMessage = @"QUOTA_EXCEEDED"; + +/** @var kAppNotVerifiedErrorMessage + @brief This is the error message the server will respond with if Firebase could not verify the + app during a phone authentication flow. + */ +static NSString *const kAppNotVerifiedErrorMessage = @"APP_NOT_VERIFIED"; + +/** @class FIRSendVerificationCodeResponseTests + @brief Tests for @c FIRSendVerificationCodeResponseTests. + */ +@interface FIRSendVerificationCodeResponseTests : XCTestCase +@end + +@implementation FIRSendVerificationCodeResponseTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testSendVerificationCodeResponseInvalidPhoneNumber + @brief Tests a failed attempt to send a verification code with an invalid phone number. + */ +- (void)testSendVerificationCodeResponseInvalidPhoneNumber { + FIRAuthAppCredential *credential = + [[FIRAuthAppCredential alloc]initWithReceipt:kTestReceipt secret:kTestSecret]; + FIRSendVerificationCodeRequest *request = + [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:kTestInvalidPhoneNumber + appCredential:credential + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRSendVerificationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend sendVerificationCode:request + callback:^(FIRSendVerificationCodeResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kInvalidPhoneNumberErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidPhoneNumber); +} + +/** @fn testSendVerificationCodeResponseQuotaExceededError + @brief Tests a failed attempt to send a verification code due to SMS quota having been exceeded. + */ +- (void)testSendVerificationCodeResponseQuotaExceededError { + FIRAuthAppCredential *credential = + [[FIRAuthAppCredential alloc]initWithReceipt:kTestReceipt secret:kTestSecret]; + FIRSendVerificationCodeRequest *request = + [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:kTestPhoneNumber + appCredential:credential + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRSendVerificationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend sendVerificationCode:request + callback:^(FIRSendVerificationCodeResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kQuotaExceededErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeQuotaExceeded); +} + +/** @fn testSendVerificationCodeResponseAppNotVerifiedError + @brief Tests a failed attempt to send a verification code due to Firebase not being able to + verify the app. + */ +- (void)testSendVerificationCodeResponseAppNotVerifiedError { + FIRAuthAppCredential *credential = + [[FIRAuthAppCredential alloc]initWithReceipt:kTestReceipt secret:kTestSecret]; + FIRSendVerificationCodeRequest *request = + [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:kTestPhoneNumber + appCredential:credential + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRSendVerificationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend sendVerificationCode:request + callback:^(FIRSendVerificationCodeResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kAppNotVerifiedErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeAppNotVerified); +} + +/** @fn testSuccessfulSendVerificationCodeResponse + @brief Tests a succesful to send a verification code. + */ +- (void)testSuccessfulSendVerificationCodeResponse { + FIRAuthAppCredential *credential = + [[FIRAuthAppCredential alloc]initWithReceipt:kTestReceipt secret:kTestSecret]; + FIRSendVerificationCodeRequest *request = + [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:kTestPhoneNumber + appCredential:credential + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRSendVerificationCodeResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend sendVerificationCode:request + callback:^(FIRSendVerificationCodeResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + [_RPCIssuer respondWithJSON:@{ + kVerificationIDKey : kFakeVerificationID + }]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.verificationID, kFakeVerificationID); +} + +@end diff --git a/Example/Auth/Tests/FIRSetAccountInfoRequestTests.m b/Example/Auth/Tests/FIRSetAccountInfoRequestTests.m new file mode 100644 index 0000000..54d8ff0 --- /dev/null +++ b/Example/Auth/Tests/FIRSetAccountInfoRequestTests.m @@ -0,0 +1,285 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRSetAccountInfoRequest.h" +#import "FIRSetAccountInfoResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kIDTokenKey + @brief The key for the "idToken" value in the request. This is actually the STS Access Token, + despite it's confusing (backwards compatiable) parameter name. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** @var kTestAccessToken + @bried Fake acess token for testing. + */ + static NSString *const kTestAccessToken = @"accessToken"; + +/** @var kDisplayNameKey + @brief The key for the "displayName" value in the request. + */ +static NSString *const kDisplayNameKey = @"displayName"; + +/** @var kTestDisplayName + @brief The fake @c displayName for testing. + */ +static NSString *const kTestDisplayName = @"testDisplayName"; + +/** @var kLocalIDKey + @brief The key for the "localID" value in the request. + */ +static NSString *const kLocalIDKey = @"localId"; + +/** @var kTestLocalID + @brief The fake @c localID for testing in the request. + */ +static NSString *const kTestLocalID = @"testLocalId"; + +/** @var kEmailKey + @brief The key for the "email" value in the request. + */ +static NSString *const kEmailKey = @"email"; + +/** @var kTestEmail + @brief The fake @c email used for testing in the request. + */ +static NSString *const ktestEmail = @"testEmail"; + +/** @var kPasswordKey + @brief The key for the "password" value in the request. + */ +static NSString *const kPasswordKey = @"password"; + +/** @var kTestPassword + @brief The fake @c password used for testing in the request. + */ +static NSString *const kTestPassword = @"testPassword"; + +/** @var kPhotoURLKey + @brief The key for the "photoURL" value in the request. + */ +static NSString *const kPhotoURLKey = @"photoUrl"; + +/** @var kTestPhotoURL + @brief The fake photoUrl for testing in the request. + */ +static NSString *const kTestPhotoURL = @"testPhotoUrl"; + +/** @var kProvidersKey + @brief The key for the "providers" value in the request. + */ +static NSString *const kProvidersKey = @"provider"; + +/** @var kTestProviders + @brief The fake @c providers value used for testing in the request. + */ +static NSString *const kTestProviders = @"testProvider"; + +/** @var kOOBCodeKey + @brief The key for the "OOBCode" value in the request. + */ +static NSString *const kOOBCodeKey = @"oobCode"; + +/** @var kTestOOBCode + @brief The fake @c OOBCode used for testing the request. + */ +static NSString *const kTestOOBCode = @"testOobCode"; + +/** @var kEmailVerifiedKey + @brief The key for the "emailVerified" value in the request. + */ +static NSString *const kEmailVerifiedKey = @"emailVerified"; + +/** @var kTestEmailVerified + @brief The fake @c emailVerified value used for testing the request. + */ +static const BOOL kTestEmailVerified = YES; + +/** @var kUpgradeToFederatedLoginKey + @brief The key for the "upgradeToFederatedLogin" value in the request. + */ +static NSString *const kUpgradeToFederatedLoginKey = @"upgradeToFederatedLogin"; + +/** @var kTestUpgradeToFederatedLogin + @brief The fake @c upgradeToFederatedLogin value for testing the request. + */ +static const BOOL kTestUpgradeToFederatedLogin = YES; + +/** @var kCaptchaChallengeKey + @brief The key for the "captchaChallenge" value in the request. + */ +static NSString *const kCaptchaChallengeKey = @"captchaChallenge"; + +/** @var kTestCaptchaChallenge + @brief The fake @c captchaChallenge for testing in the request. + */ +static NSString *const kTestCaptchaChallenge = @"TestCaptchaChallenge"; + +/** @var kCaptchaResponseKey + @brief The key for the "captchaResponse" value the request. + */ +static NSString *const kCaptchaResponseKey = @"captchaResponse"; + +/** @var kTestCaptchaResponse + @brief The fake @c captchaResponse for testing the request. + */ +static NSString *const kTestCaptchaResponse = @"TestCaptchaResponse"; + +/** @var kDeleteAttributesKey + @brief The key for the "deleteAttribute" value in the request. + */ +static NSString *const kDeleteAttributesKey = @"deleteAttribute"; + +/** @var kTestDeleteAttributes + @brief The fake @c deleteAttribute value for testing the request. + */ +static NSString *const kTestDeleteAttributes = @"TestDeleteAttributes"; + +/** @var kDeleteProvidersKey + @brief The key for the "deleteProvider" value in the request. + */ +static NSString *const kDeleteProvidersKey = @"deleteProvider"; + +/** @var kTestDeleteProviders + @brief The fake @c deleteProviders for testing the request. + */ +static NSString *const kTestDeleteProviders = @"TestDeleteProviders"; + +/** @var kReturnSecureTokenKey + @brief The key for the "returnSecureToken" value in the request. + */ +static NSString *const kReturnSecureTokenKey = @"returnSecureToken"; + +/** @var kExpectedAPIURL + @brief The expected URL for test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccountInfo?key=APIKey"; + +/** @class FIRSetAccountInfoRequestTests + @brief Tests for @c FIRSetAccountInfoRequest. + */ +@interface FIRSetAccountInfoRequestTests : XCTestCase +@end +@implementation FIRSetAccountInfoRequestTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testSetAccountInfoRequest + @brief Tests the set account info request. + */ +- (void)testSetAccountInfoRequest { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + request.returnSecureToken = NO; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + }]; + + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]); + XCTAssertNil(_RPCIssuer.decodedRequest[kIDTokenKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kDisplayNameKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kLocalIDKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kEmailKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kPasswordKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kPhotoURLKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kProvidersKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kOOBCodeKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kEmailVerifiedKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kUpgradeToFederatedLoginKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kCaptchaChallengeKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kCaptchaResponseKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kDeleteAttributesKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kDeleteProvidersKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kReturnSecureTokenKey]); + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); +} + +/** @fn testSetAccountInfoRequestOptionalFields + @brief Tests the set account info request with optional fields. + */ +- (void)testSetAccountInfoRequestOptionalFields { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + request.accessToken = kTestAccessToken; + request.displayName = kTestDisplayName; + request.localID = kTestLocalID; + request.email = ktestEmail; + request.password = kTestPassword; + request.providers = @[ kTestProviders ]; + request.OOBCode = kTestOOBCode; + request.emailVerified = kTestEmailVerified; + request.photoURL = [NSURL URLWithString:kTestPhotoURL]; + request.upgradeToFederatedLogin = kTestUpgradeToFederatedLogin; + request.captchaChallenge = kTestCaptchaChallenge; + request.captchaResponse = kTestCaptchaResponse; + request.deleteAttributes = @[ kTestDeleteAttributes ]; + request.deleteProviders = @[ kTestDeleteProviders ]; + + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + }]; + + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIDTokenKey], kTestAccessToken); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kDisplayNameKey], kTestDisplayName); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kLocalIDKey], kTestLocalID); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kEmailKey], ktestEmail); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPasswordKey], kTestPassword); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPhotoURLKey], kTestPhotoURL); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kProvidersKey], @[ kTestProviders ]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kOOBCodeKey], kTestOOBCode); + XCTAssert(_RPCIssuer.decodedRequest[kEmailVerifiedKey]); + XCTAssert(_RPCIssuer.decodedRequest[kUpgradeToFederatedLoginKey]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kCaptchaChallengeKey], kTestCaptchaChallenge); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kCaptchaResponseKey], kTestCaptchaResponse); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kDeleteAttributesKey], + @[ kTestDeleteAttributes ]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kDeleteProvidersKey], @[ kTestDeleteProviders ]); + XCTAssertTrue([_RPCIssuer.decodedRequest[kReturnSecureTokenKey] boolValue]); + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); +} + +@end diff --git a/Example/Auth/Tests/FIRSetAccountInfoResponseTests.m b/Example/Auth/Tests/FIRSetAccountInfoResponseTests.m new file mode 100644 index 0000000..d650f13 --- /dev/null +++ b/Example/Auth/Tests/FIRSetAccountInfoResponseTests.m @@ -0,0 +1,530 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRSetAccountInfoRequest.h" +#import "FIRSetAccountInfoResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kEmailExistsErrorMessage + @brief This is the error message the server will respond with if the user entered an invalid + email address. + */ +static NSString *const kEmailExistsErrorMessage = @"EMAIL_EXISTS"; + +/** @var kVerifiedProviderKey + @brief The name of the "VerifiedProvider" property in the response. + */ +static NSString *const kProviderUserInfoKey = @"providerUserInfo"; + +/** @var kPhotoUrlKey + @brief The name of the "photoURL" property in the response. + */ +static NSString *const kPhotoUrlKey = @"photoUrl"; + +/** @var kTestPhotoURL + @brief The fake photoUrl property value in the response. + */ +static NSString *const kTestPhotoURL = @"testPhotoURL"; + +/** @var kIDTokenKey + @brief The name of the "IDToken" property in the response. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** @var kTestIDToken + @brief Testing ID token for verifying assertion. + */ +static NSString *const kTestIDToken = @"ID_TOKEN"; + +/** @var kExpiresInKey + @brief The name of the "expiresIn" property in the response. + */ +static NSString *const kExpiresInKey = @"expiresIn"; + +/** @var kTestExpiresIn + @brief Fake token expiration time. + */ +static NSString *const kTestExpiresIn = @"12345"; + +/** @var kRefreshTokenKey + @brief The name of the "refreshToken" property in the response. + */ +static NSString *const kRefreshTokenKey = @"refreshToken"; + +/** @var kTestRefreshToken + @brief Fake refresh token. + */ +static NSString *const kTestRefreshToken = @"REFRESH_TOKEN"; + +/** @var kEmailSignUpNotAllowedErrorMessage + @brief This is the error message the server will respond with if admin disables password + account. + */ +static NSString *const kEmailSignUpNotAllowedErrorMessage = @"OPERATION_NOT_ALLOWED"; + +/** @var kPasswordLoginDisabledErrorMessage + @brief This is the error message the server responds with if password login is disabled. + */ +static NSString *const kPasswordLoginDisabledErrorMessage = @"PASSWORD_LOGIN_DISABLED"; + +/** @var kCredentialTooOldErrorMessage + @brief This is the error message the server responds with if account change is attempted 5 + minutes after signing in. + */ +static NSString *const kCredentialTooOldErrorMessage = @"CREDENTIAL_TOO_OLD_LOGIN_AGAIN"; + +/** @var kinvalidUserTokenErrorMessage + @brief This is the error message the server will respond with if the user's saved auth + credential is invalid, the user has to sign-in again. + */ +static NSString *const kinvalidUserTokenErrorMessage = @"INVALID_ID_TOKEN"; + +/** @var kUserDisabledErrorMessage + @brief This is the error message the server will respond with if the user's account has been + disabled. + */ +static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED"; + +/** @var kInvalidEmailErrorMessage + @brief The error returned by the server if the email is invalid. + */ +static NSString *const kInvalidEmailErrorMessage = @"INVALID_EMAIL"; + +/** @var kWeakPasswordErrorMessage + @brief This is the error message the server will respond with if the user's new password + is too weak that it is too short. + */ +static NSString *const kWeakPasswordErrorMessage = + @"WEAK_PASSWORD : Password should be at least 6 characters"; + +/** @var kWeakPasswordClientErrorMessage + @brief This is the error message the client will see if the user's new password is too weak + that it is too short. + @remarks This message should be derived from @c kWeakPasswordErrorMessage . + */ +static NSString *const kWeakPasswordClientErrorMessage = + @"Password should be at least 6 characters"; + +/** @var kExpiredActionCodeErrorMessage + @brief This is the error message the server will respond with if the action code is expired. + */ +static NSString *const kExpiredActionCodeErrorMessage = @"EXPIRED_OOB_CODE:"; + +/** @var kInvalidActionCodeErrorMessage + @brief This is the error message the server will respond with if the action code is invalid. + */ +static NSString *const kInvalidActionCodeErrorMessage = @"INVALID_OOB_CODE"; + +/** @var kInvalidMessagePayloadErrorMessage + @brief This is the prefix for the error message the server responds with if an invalid message + payload was sent. + */ +static NSString *const kInvalidMessagePayloadErrorMessage = @"INVALID_MESSAGE_PAYLOAD"; + +/** @var kInvalidSenderErrorMessage + @brief This is the prefix for the error message the server responds with if invalid sender is + used to send the email for updating user's email address. + */ +static NSString *const kInvalidSenderErrorMessage = @"INVALID_SENDER"; + +/** @var kInvalidRecipientEmailErrorMessage + @brief This is the prefix for the error message the server responds with if the recipient email + is invalid. + */ +static NSString *const kInvalidRecipientEmailErrorMessage = @"INVALID_RECIPIENT_EMAIL"; + +/** @var kEpsilon + @brief Allowed difference when comparing floating point numbers. + */ +static const double kEpsilon = 1e-3; + +/** @class FIRSetAccountInfoResponseTests + @brief Tests for @c FIRSetAccountInfoResponse. + */ +@interface FIRSetAccountInfoResponseTests : XCTestCase +@end +@implementation FIRSetAccountInfoResponseTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testEmailExistsError + @brief This test simulates @c testSignUpNewUserEmailExistsError with @c + FIRAuthErrorCodeEmailExists error. + */ +- (void)testEmailExistsError { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kEmailExistsErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeEmailAlreadyInUse); +} + +/** @fn testEmailSignUpNotAllowedError + @brief This test simulates @c testEmailSignUpNotAllowedError with @c + FIRAuthErrorCodeOperationNotAllowed error. + */ +- (void)testEmailSignUpNotAllowedError { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kEmailSignUpNotAllowedErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed); +} + +/** @fn testPasswordLoginDisabledError + @brief This test simulates @c passwordLoginDisabledError with @c + FIRAuthErrorCodeOperationNotAllowed error. + */ +- (void)testPasswordLoginDisabledError { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kPasswordLoginDisabledErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed); +} + +/** @fn testUserDisabledError + @brief This test simulates @c testUserDisabledError with @c FIRAuthErrorCodeUserDisabled error. + */ +- (void)testUserDisabledError { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kUserDisabledErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserDisabled); +} + +/** @fn testInvalidUserTokenError + @brief This test simulates @c testinvalidUserTokenError with @c + FIRAuthErrorCodeCredentialTooOld error. + */ +- (void)testInvalidUserTokenError { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kinvalidUserTokenErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidUserToken); +} + +/** @fn testrequiresRecentLogin + @brief This test simulates @c testCredentialTooOldError with @c + FIRAuthErrorCodeRequiresRecentLogin error. + */ +- (void)testrequiresRecentLogin { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kCredentialTooOldErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeRequiresRecentLogin); +} + +/** @fn testWeakPasswordError + @brief This test simulates @c FIRAuthErrorCodeWeakPassword error. + */ +- (void)testWeakPasswordError { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kWeakPasswordErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeWeakPassword); + XCTAssertEqualObjects(RPCError.userInfo[NSLocalizedFailureReasonErrorKey], + kWeakPasswordClientErrorMessage); +} + +/** @fn testInvalidEmailError + @brief This test simulates @c FIRAuthErrorCodeInvalidEmail error code. + */ +- (void)testInvalidEmailError { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kInvalidEmailErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidEmail); +} + +/** @fn testInvalidActionCodeError + @brief This test simulates @c FIRAuthErrorCodeInvalidActionCode error code. + */ +- (void)testInvalidActionCodeError { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kInvalidActionCodeErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidActionCode); +} + +/** @fn testExpiredActionCodeError + @brief This test simulates @c FIRAuthErrorCodeExpiredActionCode error code. + */ +- (void)testExpiredActionCodeError { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kExpiredActionCodeErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeExpiredActionCode); +} + +/** @fn testInvalidMessagePayloadError + @brief Tests for @c FIRAuthErrorCodeInvalidMessagePayload. + */ +- (void)testInvalidMessagePayloadError { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kInvalidMessagePayloadErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidMessagePayload); +} + +/** @fn testInvalidSenderError + @brief Tests for @c FIRAuthErrorCodeInvalidSender. + */ +- (void)testInvalidSenderError { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kInvalidSenderErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidSender); +} + +/** @fn testInvalidRecipientEmailError + @brief Tests for @c FIRAuthErrorCodeInvalidRecipientEmail. + */ +- (void)testInvalidRecipientEmailError { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kInvalidRecipientEmailErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidRecipientEmail); +} + +/** @fn testSuccessfulSetAccountInfoResponse + @brief This test simulates a successful @c SetAccountInfo flow. + */ +- (void)testSuccessfulSetAccountInfoResponse { + FIRSetAccountInfoRequest *request = [[FIRSetAccountInfoRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSetAccountInfoResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend setAccountInfo:request + callback:^(FIRSetAccountInfoResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + kProviderUserInfoKey:@[ + @{ kPhotoUrlKey : kTestPhotoURL } + ], + kIDTokenKey : kTestIDToken, + kExpiresInKey : kTestExpiresIn, + kRefreshTokenKey : kTestRefreshToken + }]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + if ([RPCResponse.providerUserInfo count]) { + NSURL *responsePhotoUrl = RPCResponse.providerUserInfo[0].photoURL; + XCTAssertEqualObjects(responsePhotoUrl.absoluteString, kTestPhotoURL); + } + XCTAssertEqualObjects(RPCResponse.IDToken, kTestIDToken); + NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow]; + XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon); + XCTAssertEqualObjects(RPCResponse.refreshToken, kTestRefreshToken); +} + +@end diff --git a/Example/Auth/Tests/FIRSignUpNewUserRequestTests.m b/Example/Auth/Tests/FIRSignUpNewUserRequestTests.m new file mode 100644 index 0000000..622ec7c --- /dev/null +++ b/Example/Auth/Tests/FIRSignUpNewUserRequestTests.m @@ -0,0 +1,140 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRGetOOBConfirmationCodeResponse.h" +#import "FIRSignUpNewUserRequest.h" +#import "FIRSignUpNewUserResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kExpectedAPIURL + @brief The expected URL for the test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key=APIKey"; + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kEmailKey + @brief The name of the "email" property in the request. + */ +static NSString *const kEmailKey = @"email"; + +/** @var kTestEmail + @brief Testing user email adadress. + */ +static NSString *const kTestEmail = @"test@gmail.com"; + +/** @var kDisplayNameKey + @brief the name of the "displayName" property in the request. + */ +static NSString *const kDisplayNameKey = @"displayName"; + +/** @var kTestDisplayName + @brief Testing display name. + */ +static NSString *const kTestDisplayName = @"DisplayName"; + +/** @var kPasswordKey + @brief the name of the "password" property in the request. + */ +static NSString *const kPasswordKey = @"password"; + +/** @var kTestPassword + @brief Testing password. + */ +static NSString *const kTestPassword = @"Password"; + +/** @var kReturnSecureTokenKey + @brief The key for the "returnSecureToken" value in the request. + */ +static NSString *const kReturnSecureTokenKey = @"returnSecureToken"; + +@interface FIRSignUpNewUserRequestTests : XCTestCase + +@end + +@implementation FIRSignUpNewUserRequestTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testSignUpNewUserRequestAnonymous + @brief Tests the encoding of a sign up new user request when user is signed in anonymously. + */ +- (void)testSignUpNewUserRequestAnonymous { + FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey]; + request.returnSecureToken = NO; + [FIRAuthBackend signUpNewUser:request + callback:^(FIRSignUpNewUserResponse *_Nullable response, + NSError *_Nullable error) { + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]); + XCTAssertNil(_RPCIssuer.decodedRequest[kEmailKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kDisplayNameKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kPasswordKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kReturnSecureTokenKey]); +} + +/** @fn testSignUpNewUserRequestNotAnonymous + @brief Tests the encoding of a sign up new user request when user is not signed in anonymously. + */ +- (void)testSignUpNewUserRequestNotAnonymous { + FIRSignUpNewUserRequest *request = + [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey + email:kTestEmail + password:kTestPassword + displayName:kTestDisplayName]; + [FIRAuthBackend signUpNewUser:request + callback:^(FIRSignUpNewUserResponse *_Nullable response, + NSError *_Nullable error) { + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssert([_RPCIssuer.decodedRequest isKindOfClass:[NSDictionary class]]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kEmailKey], kTestEmail); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPasswordKey], kTestPassword); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kDisplayNameKey], kTestDisplayName); + XCTAssertTrue([_RPCIssuer.decodedRequest[kReturnSecureTokenKey] boolValue]); +} + +@end diff --git a/Example/Auth/Tests/FIRSignUpNewUserResponseTests.m b/Example/Auth/Tests/FIRSignUpNewUserResponseTests.m new file mode 100644 index 0000000..89479f7 --- /dev/null +++ b/Example/Auth/Tests/FIRSignUpNewUserResponseTests.m @@ -0,0 +1,291 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRSignUpNewUserRequest.h" +#import "FIRSignUpNewUserResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kIDTokenKey + @brief The name of the "IDToken" property in the response. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** @var kTestIDToken + @brief Testing ID token for verifying assertion. + */ +static NSString *const kTestIDToken = @"ID_TOKEN"; + +/** @var kExpiresInKey + @brief The name of the "expiresIn" property in the response. + */ +static NSString *const kExpiresInKey = @"expiresIn"; + +/** @var kTestExpiresIn + @brief Fake token expiration time. + */ +static NSString *const kTestExpiresIn = @"12345"; + +/** @var kRefreshTokenKey + @brief The name of the "refreshToken" property in the response. + */ +static NSString *const kRefreshTokenKey = @"refreshToken"; + +/** @var kTestRefreshToken + @brief Fake refresh token. + */ +static NSString *const kTestRefreshToken = @"REFRESH_TOKEN"; + +/** @var kTestEmail + @brief Testing user email adadress. + */ +static NSString *const kTestEmail = @"test@gmail.com"; + +/** @var kTestDisplayName + @brief Testing display name. + */ +static NSString *const kTestDisplayName = @"DisplayName"; + +/** @var kTestPassword + @brief Testing password. + */ +static NSString *const kTestPassword = @"Password"; + +/** @var kEmailAlreadyInUseErrorMessage + @brief This is the error message the server will respond with if the user entered an invalid + email address. + */ +static NSString *const kEmailAlreadyInUseErrorMessage = @"EMAIL_EXISTS"; + +/** @var kOperationNotAllowedErrorMessage + @brief This is the error message the server will respond with if user/password account was + disabled by the administrator. + */ +static NSString *const kEmailSignUpNotAllowedErrorMessage = @"OPERATION_NOT_ALLOWED"; + +/** @var kPasswordLoginDisabledErrorMessage + @brief This is the error message the server responds with if password login is disabled. + */ +static NSString *const kPasswordLoginDisabledErrorMessage = @"PASSWORD_LOGIN_DISABLED:"; + +/** @var kInvalidEmailErrorMessage + @brief The error returned by the server if the email is invalid. + */ +static NSString *const kInvalidEmailErrorMessage = @"INVALID_EMAIL"; + +/** @var kWeakPasswordErrorMessage + @brief This is the error message the server will respond with if the new user's password + is too weak that it is too short. + */ +static NSString *const kWeakPasswordErrorMessage = + @"WEAK_PASSWORD : Password should be at least 6 characters"; + +/** @var kWeakPasswordClientErrorMessage + @brief This is the error message the client will see if the new user's password is too weak + that it is too short. + @remarks This message should be derived from @c kWeakPasswordErrorMessage . + */ +static NSString *const kWeakPasswordClientErrorMessage = + @"Password should be at least 6 characters"; + +/** @var kEpsilon + @brief Allowed difference when comparing floating point numbers. + */ +static const double kEpsilon = 1e-3; + +@interface FIRSignUpNewUserResponseTests : XCTestCase +@end +@implementation FIRSignUpNewUserResponseTests + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testSuccessfulSignUp + @brief This test simulates a complete sign up flow with no errors. + */ +- (void)testSuccessfulSignUp { + FIRSignUpNewUserRequest *request = + [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey + email:kTestEmail + password:kTestPassword + displayName:kTestDisplayName]; + + __block BOOL callbackInvoked; + __block FIRSignUpNewUserResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend signUpNewUser:request + callback:^(FIRSignUpNewUserResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + kIDTokenKey : kTestIDToken, + kExpiresInKey : kTestExpiresIn, + kRefreshTokenKey : kTestRefreshToken + }]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.IDToken, kTestIDToken); + NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow]; + XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon); + XCTAssertEqualObjects(RPCResponse.refreshToken, kTestRefreshToken); + XCTAssertNil(RPCError, "There should be no error"); +} + +/** @fn testSignUpNewUserEmailAlreadyInUseError + @brief This test simulates @c testSignUpNewUserEmailAlreadyInUseError with @c + FIRAuthErrorCodeEmailAlreadyInUse error. + */ +- (void)testSignUpNewUserEmailAlreadyInUseError { + FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSignUpNewUserResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend signUpNewUser:request + callback:^(FIRSignUpNewUserResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + [_RPCIssuer respondWithServerErrorMessage:kEmailAlreadyInUseErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeEmailAlreadyInUse); +} + +/** @fn testSignUpNewUserOperationNotAllowedError + @brief This test simulates @c testSignUpNewUserEmailExistsError with @c + FIRAuthErrorCodeOperationNotAllowed error. + */ +- (void)testSignUpNewUserOperationNotAllowedError { + FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSignUpNewUserResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend signUpNewUser:request + callback:^(FIRSignUpNewUserResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + [_RPCIssuer respondWithServerErrorMessage:kEmailSignUpNotAllowedErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed); +} + +/** @fn testSignUpNewUserPasswordLoginDisabledError + @brief This test simulates @c signUpNewUserPasswordLoginDisabledError with @c + FIRAuthErrorCodeOperationNotAllowed error. + */ +- (void)testSignUpNewUserPasswordLoginDisabledError { + FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSignUpNewUserResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend signUpNewUser:request + callback:^(FIRSignUpNewUserResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + [_RPCIssuer respondWithServerErrorMessage:kPasswordLoginDisabledErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed); +} + +/** @fn testinvalidEmailError + @brief This test simulates making a request containing an invalid email address and receiving @c + FIRAuthErrorInvalidEmail error as a result. + */ +- (void)testinvalidEmailError { + FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSignUpNewUserResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend signUpNewUser:request + callback:^(FIRSignUpNewUserResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + [_RPCIssuer respondWithServerErrorMessage:kInvalidEmailErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidEmail); +} + +/** @fn testSignUpNewUserWeakPasswordError + @brief This test simulates @c FIRAuthErrorCodeWeakPassword error. + */ +- (void)testSignUpNewUserWeakPasswordError { + FIRSignUpNewUserRequest *request = [[FIRSignUpNewUserRequest alloc] initWithAPIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRSignUpNewUserResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend signUpNewUser:request + callback:^(FIRSignUpNewUserResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + [_RPCIssuer respondWithServerErrorMessage:kWeakPasswordErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeWeakPassword); + XCTAssertEqualObjects(RPCError.userInfo[NSLocalizedFailureReasonErrorKey], + kWeakPasswordClientErrorMessage); +} + +@end diff --git a/Example/Auth/Tests/FIRTwitterAuthProviderTests.m b/Example/Auth/Tests/FIRTwitterAuthProviderTests.m new file mode 100644 index 0000000..da02c43 --- /dev/null +++ b/Example/Auth/Tests/FIRTwitterAuthProviderTests.m @@ -0,0 +1,60 @@ +/* + * Copyright 2017 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 "Twitter/FIRTwitterAuthProvider.h" +#import "FIRAuthCredential_Internal.h" +#import "FIRVerifyAssertionRequest.h" + +/** @var kTwitterToken + @brief A testing Twitter token. + */ +static NSString *const kTwitterToken = @"Token"; + +/** @var kTwitterSecret + @brief A testing Twitter secret. + */ +static NSString *const kTwitterSecret = @"Secret"; + +/** @var kAPIKey + @brief A testing API Key. + */ +static NSString *const kAPIKey = @"APIKey"; + +/** @class FIRTwitterAuthProviderTests + @brief Tests for @c FIRTwitterAuthProvider + */ +@interface FIRTwitterAuthProviderTests : XCTestCase +@end +@implementation FIRTwitterAuthProviderTests + +/** @fn testCredentialWithToken + @brief Tests the @c credentialWithToken method to make sure the credential it produces populates + the appropriate fields in a verify assertion request. + */ +- (void)testCredentialWithToken { + FIRAuthCredential *credential = + [FIRTwitterAuthProvider credentialWithToken:kTwitterToken secret:kTwitterSecret]; + FIRVerifyAssertionRequest *request = + [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kAPIKey + providerID:FIRTwitterAuthProviderID]; + [credential prepareVerifyAssertionRequest:request]; + XCTAssertEqualObjects(request.providerAccessToken, kTwitterToken); + XCTAssertEqualObjects(request.providerOAuthTokenSecret, kTwitterSecret); +} + +@end diff --git a/Example/Auth/Tests/FIRUserTests.m b/Example/Auth/Tests/FIRUserTests.m new file mode 100644 index 0000000..5a4c00a --- /dev/null +++ b/Example/Auth/Tests/FIRUserTests.m @@ -0,0 +1,1801 @@ +/* + * Copyright 2017 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 "EmailPassword/FIREmailAuthProvider.h" +#import "Facebook/FIRFacebookAuthProvider.h" +#import "Google/FIRGoogleAuthProvider.h" +#import "Phone/FIRPhoneAuthCredential_Internal.h" +#import "Phone/FIRPhoneAuthProvider.h" +#import "FIRAdditionalUserInfo.h" +#import "FIRAuth.h" +#import "FIRAuthErrorUtils.h" +#import "FIRAuthGlobalWorkQueue.h" +#import "FIRUser.h" +#import "FIRUserInfo.h" +#import "FIRAuthBackend.h" +#import "FIRGetAccountInfoRequest.h" +#import "FIRGetAccountInfoResponse.h" +#import "FIRSetAccountInfoRequest.h" +#import "FIRSetAccountInfoResponse.h" +#import "FIRVerifyAssertionResponse.h" +#import "FIRVerifyAssertionRequest.h" +#import "FIRVerifyPasswordRequest.h" +#import "FIRVerifyPasswordResponse.h" +#import "FIRVerifyPhoneNumberRequest.h" +#import "FIRVerifyPhoneNumberResponse.h" +#import "FIRApp+FIRAuthUnitTests.h" +#import "OCMStubRecorder+FIRAuthUnitTests.h" +#import <OCMock/OCMock.h> + +NS_ASSUME_NONNULL_BEGIN + +/** @var kAPIKey + @brief The fake API key. + */ +static NSString *const kAPIKey = @"FAKE_API_KEY"; + +/** @var kAccessToken + @brief The fake access token. + */ +static NSString *const kAccessToken = @"ACCESS_TOKEN"; + +/** @var kNewAccessToken + @brief A new value for the fake access token. + */ +static NSString *const kNewAccessToken = @"NEW_ACCESS_TOKEN"; + +/** @var kAccessTokenValidInterval + @brief The time to live for the fake access token. + */ +static const NSTimeInterval kAccessTokenTimeToLive = 60 * 60; + +/** @var kRefreshToken + @brief The fake refresh token. + */ +static NSString *const kRefreshToken = @"REFRESH_TOKEN"; + +/** @var kLocalID + @brief The fake local user ID. + */ +static NSString *const kLocalID = @"LOCAL_ID"; + +/** @var kAnotherLocalID + @brief The fake local ID of another user. + */ +static NSString *const kAnotherLocalID = @"ANOTHER_LOCAL_ID"; + +/** @var kGoogleIDToken + @brief The fake ID token from Google Sign-In. + */ +static NSString *const kGoogleIDToken = @"GOOGLE_ID_TOKEN"; + +/** @var kFacebookIDToken + @brief The fake ID token from Facebook Sign-In. Facebook provider ID token is always nil. + */ +static NSString *const kFacebookIDToken = nil; + +/** @var kGoogleAccessToken + @brief The fake access token from Google Sign-In. + */ +static NSString *const kGoogleAccessToken = @"GOOGLE_ACCESS_TOKEN"; + +/** @var kFacebookAccessToken + @brief The fake access token from Facebook Sign-In. + */ +static NSString *const kFacebookAccessToken = @"FACEBOOK_ACCESS_TOKEN"; + +/** @var kEmail + @brief The fake user email. + */ +static NSString *const kEmail = @"user@company.com"; + +/** @var kPhoneNumber + @brief The fake user phone number. + */ +static NSString *const kPhoneNumber = @"12345658"; + +/** @var kTemporaryProof + @brief The fake temporary proof. + */ +static NSString *const kTemporaryProof = @"12345658"; + +/** @var kNewEmail + @brief A new value for the fake user email. + */ +static NSString *const kNewEmail = @"newuser@company.com"; + +/** @var kUserName + @brief The fake user name. + */ +static NSString *const kUserName = @"User Doe"; + +/** @var kNewDisplayName + @brief A new value for the fake user display name. + */ +static NSString *const kNewDisplayName = @"New User Doe"; + +/** @var kPhotoURL + @brief The fake user profile image URL string. + */ +static NSString *const kPhotoURL = @"https://host.domain/image"; + +/** @var kNewPhotoURL + @brief A new value for the fake user profile image URL string.. + */ +static NSString *const kNewPhotoURL = @"https://host.domain/new/image"; + +/** @var kPassword + @brief The fake user password. + */ +static NSString *const kPassword = @"123456"; + +/** @var kNewPassword + @brief The fake new user password. + */ +static NSString *const kNewPassword = @"1234567"; + +/** @var kPasswordHash + @brief The fake user password hash. + */ +static NSString *const kPasswordHash = @"UkVEQUNURUQ="; + +/** @var kGoogleUD + @brief The fake user ID under Google Sign-In. + */ +static NSString *const kGoogleID = @"GOOGLE_ID"; + +/** @var kGoogleEmail + @brief The fake user email under Google Sign-In. + */ +static NSString *const kGoogleEmail = @"user@gmail.com"; + +/** @var kGoogleDisplayName + @brief The fake user display name under Google Sign-In. + */ +static NSString *const kGoogleDisplayName = @"Google Doe"; + +/** @var kEmailDisplayName + @brief The fake user display name for email password user. + */ +static NSString *const kEmailDisplayName = @"Email Doe"; + +/** @var kFacebookDisplayName + @brief The fake user display name under Facebook Sign-In. + */ +static NSString *const kFacebookDisplayName = @"Facebook Doe"; + +/** @var kGooglePhotoURL + @brief The fake user profile image URL string under Google Sign-In. + */ +static NSString *const kGooglePhotoURL = @"https://googleusercontents.com/user/profile"; + +/** @var kFacebookID + @brief The fake user ID under Facebook Login. + */ +static NSString *const kFacebookID = @"FACEBOOK_ID"; + +/** @var kFacebookEmail + @brief The fake user email under Facebook Login. + */ +static NSString *const kFacebookEmail = @"user@facebook.com"; + +/** @var kVerificationCode + @brief Fake verification code used for testing. + */ +static NSString *const kVerificationCode = @"12345678"; + +/** @var kVerificationID + @brief Fake verification ID for testing. + */ +static NSString *const kVerificationID = @"55432"; + +/** @var kExpectationTimeout + @brief The maximum time waiting for expectations to fulfill. + */ +static const NSTimeInterval kExpectationTimeout = 1; + +/** @class FIRUserTests + @brief Tests for @c FIRUser . + */ +@interface FIRUserTests : XCTestCase +@end +@implementation FIRUserTests { + + /** @var _mockBackend + @brief The mock @c FIRAuthBackendImplementation . + */ + id _mockBackend; +} + +/** @fn googleProfile + @brief The fake user profile under additional user data in @c FIRVerifyAssertionResponse. + */ ++ (NSDictionary *)googleProfile { + static NSDictionary *kGoogleProfile = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + kGoogleProfile = @{ + @"email": kGoogleEmail, + @"given_name": @"User", + @"family_name": @"Doe" + }; + }); + return kGoogleProfile; +} + +- (void)setUp { + [super setUp]; + _mockBackend = OCMProtocolMock(@protocol(FIRAuthBackendImplementation)); + [FIRAuthBackend setBackendImplementation:_mockBackend]; + [FIRApp resetAppForAuthUnitTests]; +} + +- (void)tearDown { + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +#pragma mark - Tests + +/** @fn testUserProperties + @brief Tests properties of the @c FIRUser instance. + */ +- (void)testUserProperties { + // Mock auth provider user info for email/password for GetAccountInfo. + id mockPasswordUserInfo = OCMClassMock([FIRGetAccountInfoResponseProviderUserInfo class]); + OCMStub([mockPasswordUserInfo providerID]).andReturn(FIREmailAuthProviderID); + OCMStub([mockPasswordUserInfo federatedID]).andReturn(kEmail); + OCMStub([mockPasswordUserInfo email]).andReturn(kEmail); + + // Mock auth provider user info from Google for GetAccountInfo. + id mockGoogleUserInfo = OCMClassMock([FIRGetAccountInfoResponseProviderUserInfo class]); + OCMStub([mockGoogleUserInfo providerID]).andReturn(FIRGoogleAuthProviderID); + OCMStub([mockGoogleUserInfo displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGoogleUserInfo photoURL]).andReturn([NSURL URLWithString:kGooglePhotoURL]); + OCMStub([mockGoogleUserInfo federatedID]).andReturn(kGoogleID); + OCMStub([mockGoogleUserInfo email]).andReturn(kGoogleEmail); + + // Mock auth provider user info from Facebook for GetAccountInfo. + id mockFacebookUserInfo = OCMClassMock([FIRGetAccountInfoResponseProviderUserInfo class]); + OCMStub([mockFacebookUserInfo providerID]).andReturn(FIRFacebookAuthProviderID); + OCMStub([mockFacebookUserInfo federatedID]).andReturn(kFacebookID); + OCMStub([mockFacebookUserInfo email]).andReturn(kFacebookEmail); + + // Mock auth provider user info from Phone auth provider for GetAccountInfo. + id mockPhoneUserInfo = OCMClassMock([FIRGetAccountInfoResponseProviderUserInfo class]); + OCMStub([mockPhoneUserInfo providerID]).andReturn(FIRPhoneAuthProviderID); + OCMStub([mockPhoneUserInfo phoneNumber]).andReturn(kPhoneNumber); + + // Mock the root user info object for GetAccountInfo. + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser emailVerified]).andReturn(YES); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGetAccountInfoResponseUser photoURL]).andReturn([NSURL URLWithString:kPhotoURL]); + OCMStub([mockGetAccountInfoResponseUser providerUserInfo]) + .andReturn((@[ mockPasswordUserInfo, + mockGoogleUserInfo, + mockFacebookUserInfo, + mockPhoneUserInfo ])); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *user) { + // Verify FIRUserInfo properties on FIRUser itself. + XCTAssertEqualObjects(user.providerID, @"Firebase"); + XCTAssertEqualObjects(user.uid, kLocalID); + XCTAssertEqualObjects(user.displayName, kGoogleDisplayName); + XCTAssertEqualObjects(user.photoURL, [NSURL URLWithString:kPhotoURL]); + XCTAssertEqualObjects(user.email, kEmail); + + // Verify FIRUser properties besides providerData contents. + XCTAssertFalse(user.anonymous); + XCTAssertTrue(user.emailVerified); + XCTAssertEqualObjects(user.refreshToken, kRefreshToken); + XCTAssertEqual(user.providerData.count, 4u); + + NSDictionary<NSString *, id<FIRUserInfo>> *providerMap = + [self dictionaryWithUserInfoArray:user.providerData]; + + // Verify FIRUserInfo properties from email/password. + id<FIRUserInfo> passwordUserInfo = providerMap[FIREmailAuthProviderID]; + XCTAssertNotNil(passwordUserInfo); + XCTAssertEqualObjects(passwordUserInfo.uid, kEmail); + XCTAssertNil(passwordUserInfo.displayName); + XCTAssertNil(passwordUserInfo.photoURL); + XCTAssertEqualObjects(passwordUserInfo.email, kEmail); + + // Verify FIRUserInfo properties from the Google auth provider. + id<FIRUserInfo> googleUserInfo = providerMap[FIRGoogleAuthProviderID]; + XCTAssertNotNil(googleUserInfo); + XCTAssertEqualObjects(googleUserInfo.uid, kGoogleID); + XCTAssertEqualObjects(googleUserInfo.displayName, kGoogleDisplayName); + XCTAssertEqualObjects(googleUserInfo.photoURL, [NSURL URLWithString:kGooglePhotoURL]); + XCTAssertEqualObjects(googleUserInfo.email, kGoogleEmail); + + // Verify FIRUserInfo properties from the Facebook auth provider. + id<FIRUserInfo> facebookUserInfo = providerMap[FIRFacebookAuthProviderID]; + XCTAssertNotNil(facebookUserInfo); + XCTAssertEqualObjects(facebookUserInfo.uid, kFacebookID); + XCTAssertNil(facebookUserInfo.displayName); + XCTAssertNil(facebookUserInfo.photoURL); + XCTAssertEqualObjects(facebookUserInfo.email, kFacebookEmail); + + // Verify FIRUserInfo properties from the phone auth provider. + id<FIRUserInfo> phoneUserInfo = providerMap[FIRPhoneAuthProviderID]; + XCTAssertNotNil(phoneUserInfo); + XCTAssertEqualObjects(phoneUserInfo.phoneNumber, kPhoneNumber); + + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testUpdateEmailSuccess + @brief Tests the flow of a successful @c updateEmail:completion: call. + */ +- (void)testUpdateEmailSuccess { + id (^mockUserInfoWithDisplayName)(NSString *) = ^(NSString *displayName) { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(displayName); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + return mockGetAccountInfoResponseUser; + }; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id userInfoResponse = mockUserInfoWithDisplayName(kGoogleDisplayName); + [self signInWithEmailPasswordWithMockUserInfoResponse:userInfoResponse + completion:^(FIRUser *user) { + // Pretend that the display name on the server has been changed since last request. + [self + expectGetAccountInfoWithMockUserInfoResponse:mockUserInfoWithDisplayName(kNewDisplayName)]; + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request, + FIRSetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + XCTAssertEqualObjects(request.email, kNewEmail); + XCTAssertNil(request.localID); + XCTAssertNil(request.displayName); + XCTAssertNil(request.photoURL); + XCTAssertNil(request.password); + XCTAssertNil(request.providers); + XCTAssertNil(request.deleteAttributes); + XCTAssertNil(request.deleteProviders); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]); + OCMStub([mockSetAccountInfoResponse email]).andReturn(kNewEmail); + OCMStub([mockSetAccountInfoResponse displayName]).andReturn(kNewDisplayName); + callback(mockSetAccountInfoResponse, nil); + }); + }); + [user updateEmail:kNewEmail completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects(user.email, kNewEmail); + XCTAssertEqualObjects(user.displayName, kNewDisplayName); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testUpdateEmailFailure + @brief Tests the flow of a failed @c updateEmail:completion: call. + */ +- (void)testUpdateEmailFailure { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *user) { + [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser]; + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils invalidEmailErrorWithMessage:nil]); + [user updateEmail:kNewEmail completion:^(NSError *_Nullable error) { + XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidEmail); + // Email should not have changed on the client side. + XCTAssertEqualObjects(user.email, kEmail); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testUpdatePhoneSuccess + @brief Tests the flow of a successful @c updatePhoneNumberCredential:completion: call. + */ +- (void)testUpdatePhoneSuccess { + id (^mockUserInfoWithPhoneNumber)(NSString *) = ^(NSString *phoneNumber) { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + if (phoneNumber.length) { + OCMStub([mockGetAccountInfoResponseUser phoneNumber]).andReturn(phoneNumber); + } + return mockGetAccountInfoResponseUser; + }; + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id userInfoResponse = mockUserInfoWithPhoneNumber(nil); + [self signInWithEmailPasswordWithMockUserInfoResponse:userInfoResponse + completion:^(FIRUser *user) { + [self expectVerifyPhoneNumberRequestWithPhoneNumber:kPhoneNumber error:nil]; + id userInfoResponseUpdate = mockUserInfoWithPhoneNumber(kPhoneNumber); + [self expectGetAccountInfoWithMockUserInfoResponse:userInfoResponseUpdate]; + + FIRPhoneAuthCredential *credential = + [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID + verificationCode:kVerificationCode]; + [user updatePhoneNumberCredential:credential + completion:^(NSError * _Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects([FIRAuth auth].currentUser.phoneNumber, kPhoneNumber); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testUpdatePhoneNumberFailure + @brief Tests the flow of a failed @c updatePhoneNumberCredential:completion: call. + */ +- (void)testUpdatePhoneNumberFailure { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *user) { + OCMExpect([_mockBackend verifyPhoneNumber:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils invalidPhoneNumberErrorWithMessage:nil]); + FIRPhoneAuthCredential *credential = + [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID + verificationCode:kVerificationCode]; + [user updatePhoneNumberCredential:credential completion:^(NSError *_Nullable error) { + XCTAssertEqual(error.code, FIRAuthErrorCodeInvalidPhoneNumber); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testUpdatePasswordSuccess + @brief Tests the flow of a successful @c updatePassword:completion: call. + */ +- (void)testUpdatePasswordSuccess { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *user) { + [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser]; + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request, + FIRSetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + XCTAssertEqualObjects(request.password, kNewPassword); + XCTAssertNil(request.localID); + XCTAssertNil(request.displayName); + XCTAssertNil(request.photoURL); + XCTAssertNil(request.email); + XCTAssertNil(request.providers); + XCTAssertNil(request.deleteAttributes); + XCTAssertNil(request.deleteProviders); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]); + OCMStub([mockSetAccountInfoResponse displayName]).andReturn(kNewDisplayName); + callback(mockSetAccountInfoResponse, nil); + }); + }); + [user updatePassword:kNewPassword completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertFalse(user.isAnonymous); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testUpdatePasswordFailure + @brief Tests the flow of a failed @c updatePassword:completion: call. + */ +- (void)testUpdatePasswordFailure { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *user) { + [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser]; + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils userDisabledErrorWithMessage:nil]); + [user updatePassword:kNewPassword completion:^(NSError *_Nullable error) { + XCTAssertEqual(error.code, FIRAuthErrorCodeUserDisabled); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testUpdateEmptyPasswordFailure + @brief Tests the flow of a failed @c updatePassword:completion: call due to an empty password. + */ +- (void)testUpdateEmptyPasswordFailure { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *user) { + [user updatePassword:@"" completion:^(NSError *_Nullable error) { + XCTAssertEqual(error.code, FIRAuthErrorCodeWeakPassword); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + +/** @fn testChangeProfileSuccess + @brief Tests a successful user profile change flow. + */ +- (void)testChangeProfileSuccess { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGetAccountInfoResponseUser photoURL]).andReturn(kPhotoURL); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *user) { + [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser]; + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request, + FIRSetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + XCTAssertEqualObjects(request.displayName, kNewDisplayName); + XCTAssertEqualObjects(request.photoURL, [NSURL URLWithString:kNewPhotoURL]); + XCTAssertNil(request.localID); + XCTAssertNil(request.email); + XCTAssertNil(request.password); + XCTAssertNil(request.providers); + XCTAssertNil(request.deleteAttributes); + XCTAssertNil(request.deleteProviders); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]); + OCMStub([mockSetAccountInfoResponse displayName]).andReturn(kNewDisplayName); + callback(mockSetAccountInfoResponse, nil); + }); + }); + FIRUserProfileChangeRequest *profileChange = [user profileChangeRequest]; + profileChange.photoURL = [NSURL URLWithString:kNewPhotoURL]; + profileChange.displayName = kNewDisplayName; + [profileChange commitChangesWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects(user.displayName, kNewDisplayName); + XCTAssertEqualObjects(user.photoURL, [NSURL URLWithString:kNewPhotoURL]); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testChangeProfileFailure + @brief Tests a failed user profile change flow. + */ +- (void)testChangeProfileFailure { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *user) { + [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser]; + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils tooManyRequestsErrorWithMessage:nil]); + FIRUserProfileChangeRequest *profileChange = [user profileChangeRequest]; + profileChange.displayName = kNewDisplayName; + [profileChange commitChangesWithCompletion:^(NSError *_Nullable error) { + XCTAssertEqual(error.code, FIRAuthErrorCodeTooManyRequests); + XCTAssertEqualObjects(user.displayName, kGoogleDisplayName); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testReloadSuccess + @brief Tests the flow of a successful @c reloadWithCompletion: call. + */ +- (void)testReloadSuccess { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *user) { + id mockGetAccountInfoResponseUserNew = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUserNew localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUserNew email]).andReturn(kNewEmail); + OCMStub([mockGetAccountInfoResponseUserNew displayName]).andReturn(kNewDisplayName); + OCMStub([mockGetAccountInfoResponseUserNew passwordHash]).andReturn(kPasswordHash); + [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUserNew]; + [user reloadWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects(user.email, kNewEmail); + XCTAssertEqualObjects(user.displayName, kNewDisplayName); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testReloadFailure + @brief Tests the flow of a failed @c reloadWithCompletion: call. + */ +- (void)testReloadFailure { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *user) { + OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils userTokenExpiredErrorWithMessage:nil]); + [user reloadWithCompletion:^(NSError *_Nullable error) { + XCTAssertEqual(error.code, FIRAuthErrorCodeUserTokenExpired); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testReauthenticateSuccess + @brief Tests the flow of a successful @c reauthenticateWithCredential:completion: call. + */ +- (void)testReauthenticateSuccess { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *user) { + OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request, + FIRVerifyPasswordResponseCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVeriyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]); + // New authentication comes back with new access token. + OCMStub([mockVeriyPasswordResponse IDToken]).andReturn(kNewAccessToken); + OCMStub([mockVeriyPasswordResponse approximateExpirationDate]) + .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]); + OCMStub([mockVeriyPasswordResponse refreshToken]).andReturn(kRefreshToken); + callback(mockVeriyPasswordResponse, nil); + }); + }); + OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request, + FIRGetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + // Verify that the new access token is being used for subsequent requests. + XCTAssertEqualObjects(request.accessToken, kNewAccessToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]); + OCMStub([mockGetAccountInfoResponse users]).andReturn(@[ mockGetAccountInfoResponseUser ]); + callback(mockGetAccountInfoResponse, nil); + }); + }); + FIRAuthCredential *emailCredential = + [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword]; + [user reauthenticateWithCredential:emailCredential completion:^(NSError *_Nullable error) { + XCTAssertNil(error); + // Verify that the current user is unchanged. + XCTAssertEqual([FIRAuth auth].currentUser, user); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testReauthenticateAndRetrieveDataSuccess + @brief Tests the flow of a successful @c reauthenticateAndRetrieveDataWithCredential:completion: + call. + */ +- (void)testReauthenticateAndRetrieveDataSuccess { + [self expectVerifyAssertionRequest:FIRGoogleAuthProviderID + federatedID:kGoogleID + displayName:kGoogleDisplayName + profile:[[self class] googleProfile] + providerIDToken:kGoogleIDToken + providerAccessToken:kGoogleAccessToken]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *googleCredential = + [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken]; + [[FIRAuth auth] signInAndRetrieveDataWithCredential:googleCredential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserGoogle:authResult.user]; + XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]); + XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName); + XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRGoogleAuthProviderID); + XCTAssertNil(error); + + [self expectVerifyAssertionRequest:FIRGoogleAuthProviderID + federatedID:kGoogleID + displayName:kGoogleDisplayName + profile:[[self class] googleProfile] + providerIDToken:kGoogleIDToken + providerAccessToken:kGoogleAccessToken]; + + FIRAuthCredential *reauthenticateGoogleCredential = + [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken]; + [authResult.user + reauthenticateAndRetrieveDataWithCredential:reauthenticateGoogleCredential + completion:^(FIRAuthDataResult *_Nullable + reauthenticateAuthResult, + NSError *_Nullable error) { + XCTAssertNil(error); + // Verify that the current user is unchanged. + XCTAssertEqual([FIRAuth auth].currentUser, authResult.user); + // Verify that the current user and reauthenticated user are not same pointers. + XCTAssertNotEqualObjects(authResult.user, reauthenticateAuthResult.user); + // Verify that anyway the current user and reauthenticated user have same IDs. + XCTAssertEqualObjects(authResult.user.uid, reauthenticateAuthResult.user.uid); + XCTAssertEqualObjects(authResult.user.displayName, reauthenticateAuthResult.user.displayName); + XCTAssertEqualObjects(reauthenticateAuthResult.additionalUserInfo.profile, + [[self class] googleProfile]); + XCTAssertEqualObjects(reauthenticateAuthResult.additionalUserInfo.username, kUserName); + XCTAssertEqualObjects(reauthenticateAuthResult.additionalUserInfo.providerID, + FIRGoogleAuthProviderID); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUserGoogle:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testReauthenticateFailure + @brief Tests the flow of a failed @c reauthenticateWithCredential:completion: call. + */ +- (void)testReauthenticateFailure { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *user) { + OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request, + FIRVerifyPasswordResponseCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVeriyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]); + OCMStub([mockVeriyPasswordResponse IDToken]).andReturn(kNewAccessToken); + OCMStub([mockVeriyPasswordResponse approximateExpirationDate]) + .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]); + OCMStub([mockVeriyPasswordResponse refreshToken]).andReturn(kRefreshToken); + callback(mockVeriyPasswordResponse, nil); + }); + }); + OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request, + FIRGetAccountInfoResponseCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGetAccountInfoResponseUserNew = OCMClassMock([FIRGetAccountInfoResponseUser class]); + // The newly-signed-in user has a different ID. + OCMStub([mockGetAccountInfoResponseUserNew localID]).andReturn(kAnotherLocalID); + OCMStub([mockGetAccountInfoResponseUserNew email]).andReturn(kNewEmail); + OCMStub([mockGetAccountInfoResponseUserNew displayName]).andReturn(kNewDisplayName); + OCMStub([mockGetAccountInfoResponseUserNew passwordHash]).andReturn(kPasswordHash); + id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]); + OCMStub([mockGetAccountInfoResponse users]) + .andReturn(@[ mockGetAccountInfoResponseUserNew ]); + callback(mockGetAccountInfoResponse, nil); + }); + }); + FIRAuthCredential *emailCredential = + [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword]; + [user reauthenticateWithCredential:emailCredential completion:^(NSError *_Nullable error) { + // Verify user mismatch error. + XCTAssertEqual(error.code, FIRAuthErrorCodeUserMismatch); + // Verify that the current user is unchanged. + XCTAssertEqual([FIRAuth auth].currentUser, user); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testReauthenticateUserMismatchFailure + @brief Tests the flow of a failed @c reauthenticateWithCredential:completion: call due to trying + to reauthenticate a user that does not exist. + */ +- (void)testReauthenticateUserMismatchFailure { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kGoogleDisplayName); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [self signInWithEmailPasswordWithMockUserInfoResponse:mockGetAccountInfoResponseUser + completion:^(FIRUser *user) { + OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request, + FIRVerifyAssertionResponseCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback(nil, [FIRAuthErrorUtils userNotFoundErrorWithMessage:nil]); + }); + }); + FIRAuthCredential *googleCredential = + [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken]; + [user reauthenticateWithCredential:googleCredential completion:^(NSError *_Nullable error) { + // Verify user mismatch error. + XCTAssertEqual(error.code, FIRAuthErrorCodeUserMismatch); + // Verify that the current user is unchanged. + XCTAssertEqual([FIRAuth auth].currentUser, user); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testlinkAndRetrieveDataSuccess + @brief Tests the flow of a successful @c linkAndRetrieveDataWithCredential:completion: + call. + */ +- (void)testlinkAndRetrieveDataSuccess { + [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID + federatedID:kFacebookID + displayName:kFacebookDisplayName + profile:[[self class] googleProfile] + providerIDToken:kFacebookIDToken + providerAccessToken:kFacebookAccessToken]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *facebookCredential = + [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken]; + [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserFacebook:authResult.user]; + XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]); + XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName); + XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID); + XCTAssertNil(error); + + [self expectVerifyAssertionRequest:FIRGoogleAuthProviderID + federatedID:kGoogleID + displayName:kGoogleDisplayName + profile:[[self class] googleProfile] + providerIDToken:kGoogleIDToken + providerAccessToken:kGoogleAccessToken]; + + FIRAuthCredential *linkGoogleCredential = + [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken]; + [authResult.user linkAndRetrieveDataWithCredential:linkGoogleCredential + completion:^(FIRAuthDataResult *_Nullable + linkAuthResult, + NSError *_Nullable error) { + XCTAssertNil(error); + // Verify that the current user is unchanged. + XCTAssertEqual([FIRAuth auth].currentUser, authResult.user); + // Verify that the current user and reauthenticated user are same pointers. + XCTAssertEqualObjects(authResult.user, linkAuthResult.user); + // Verify that anyway the current user and linked user have same IDs. + XCTAssertEqualObjects(authResult.user.uid, linkAuthResult.user.uid); + XCTAssertEqualObjects(authResult.user.displayName, linkAuthResult.user.displayName); + XCTAssertEqualObjects(linkAuthResult.additionalUserInfo.profile, + [[self class] googleProfile]); + XCTAssertEqualObjects(linkAuthResult.additionalUserInfo.username, kUserName); + XCTAssertEqualObjects(linkAuthResult.additionalUserInfo.providerID, + FIRGoogleAuthProviderID); + [expectation fulfill]; + }]; + + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUserGoogle:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testlinkAndRetrieveDataError + @brief Tests the flow of an unsuccessful @c linkAndRetrieveDataWithCredential:completion: + call with an error from the backend. + */ +- (void)testlinkAndRetrieveDataError { + [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID + federatedID:kFacebookID + displayName:kFacebookDisplayName + profile:[[self class] googleProfile] + providerIDToken:kFacebookIDToken + providerAccessToken:kFacebookAccessToken]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *facebookCredential = + [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken]; + [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserFacebook:authResult.user]; + XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]); + XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName); + XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID); + XCTAssertNil(error); + + OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request, + FIRVerifyAssertionResponseCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback(nil, [FIRAuthErrorUtils userDisabledErrorWithMessage:nil]); + }); + }); + + FIRAuthCredential *linkGoogleCredential = + [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken]; + [authResult.user linkAndRetrieveDataWithCredential:linkGoogleCredential + completion:^(FIRAuthDataResult *_Nullable + linkAuthResult, + NSError *_Nullable error) { + XCTAssertNil(linkAuthResult); + XCTAssertEqual(error.code, FIRAuthErrorCodeUserDisabled); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testlinkAndRetrieveDataProviderAlreadyLinked + @brief Tests the flow of an unsuccessful @c linkAndRetrieveDataWithCredential:completion: + call with FIRAuthErrorCodeProviderAlreadyLinked, which is a client side error. + */ +- (void)testlinkAndRetrieveDataProviderAlreadyLinked { + [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID + federatedID:kFacebookID + displayName:kFacebookDisplayName + profile:[[self class] googleProfile] + providerIDToken:kFacebookIDToken + providerAccessToken:kFacebookAccessToken]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *facebookCredential = + [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken]; + [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserFacebook:authResult.user]; + XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]); + XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName); + XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID); + XCTAssertNil(error); + + FIRAuthCredential *linkFacebookCredential = + [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken]; + [authResult.user linkAndRetrieveDataWithCredential:linkFacebookCredential + completion:^(FIRAuthDataResult *_Nullable + linkAuthResult, + NSError *_Nullable error) { + XCTAssertNil(linkAuthResult); + XCTAssertEqual(error.code, FIRAuthErrorCodeProviderAlreadyLinked); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testlinkEmailAndRetrieveDataSuccess + @brief Tests the flow of a successful @c linkAndRetrieveDataWithCredential:completion: + invocation for email credential. + */ +- (void)testlinkEmailAndRetrieveDataSuccess { + [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID + federatedID:kFacebookID + displayName:kFacebookDisplayName + profile:[[self class] googleProfile] + providerIDToken:kFacebookIDToken + providerAccessToken:kFacebookAccessToken]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *facebookCredential = + [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken]; + [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserFacebook:authResult.user]; + XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]); + XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName); + XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID); + XCTAssertNil(error); + + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kEmailDisplayName); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + // Get account info is expected to be invoked twice. + [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser]; + [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser]; + + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request, + FIRSetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + XCTAssertEqualObjects(request.password, kPassword); + XCTAssertNil(request.localID); + XCTAssertNil(request.displayName); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]); + callback(mockSetAccountInfoResponse, nil); + }); + }); + + FIRAuthCredential *linkEmailCredential = + [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword]; + [authResult.user linkAndRetrieveDataWithCredential:linkEmailCredential + completion:^(FIRAuthDataResult *_Nullable + linkAuthResult, + NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects(linkAuthResult.user.email, kEmail); + XCTAssertEqualObjects(linkAuthResult.user.displayName, kEmailDisplayName); + [expectation fulfill]; + }]; + + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testlinkEmailProviderAlreadyLinkedError + @brief Tests the flow of an unsuccessful @c linkAndRetrieveDataWithCredential:completion: + invocation for email credential and FIRAuthErrorCodeProviderAlreadyLinked which is a client + side error. + */ +- (void)testlinkEmailProviderAlreadyLinkedError { + [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID + federatedID:kFacebookID + displayName:kFacebookDisplayName + profile:[[self class] googleProfile] + providerIDToken:kFacebookIDToken + providerAccessToken:kFacebookAccessToken]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *facebookCredential = + [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken]; + [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserFacebook:authResult.user]; + XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]); + XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName); + XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID); + XCTAssertNil(error); + + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(kEmailDisplayName); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + // Get account info is expected to be invoked twice. + [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser]; + [self expectGetAccountInfoWithMockUserInfoResponse:mockGetAccountInfoResponseUser]; + + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request, + FIRSetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + XCTAssertEqualObjects(request.password, kPassword); + XCTAssertNil(request.localID); + XCTAssertNil(request.displayName); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]); + callback(mockSetAccountInfoResponse, nil); + }); + }); + + FIRAuthCredential *linkEmailCredential = + [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword]; + [authResult.user linkAndRetrieveDataWithCredential:linkEmailCredential + completion:^(FIRAuthDataResult *_Nullable + linkAuthResult, + NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects(linkAuthResult.user.email, kEmail); + XCTAssertEqualObjects(linkAuthResult.user.displayName, kEmailDisplayName); + + // Try linking same credential a second time to trigger client side error. + [authResult.user linkAndRetrieveDataWithCredential:linkEmailCredential + completion:^(FIRAuthDataResult *_Nullable + linkAuthResult, + NSError *_Nullable error) { + XCTAssertNil(linkAuthResult); + XCTAssertEqual(error.code, FIRAuthErrorCodeProviderAlreadyLinked); + [expectation fulfill]; + }]; + }]; + + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testlinkEmailAndRetrieveDataError + @brief Tests the flow of an unsuccessful @c linkAndRetrieveDataWithCredential:completion: + invocation for email credential and an error from the backend. + */ +- (void)testlinkEmailAndRetrieveDataError { + [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID + federatedID:kFacebookID + displayName:kFacebookDisplayName + profile:[[self class] googleProfile] + providerIDToken:kFacebookIDToken + providerAccessToken:kFacebookAccessToken]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *facebookCredential = + [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken]; + [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserFacebook:authResult.user]; + XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]); + XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName); + XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID); + XCTAssertNil(error); + + OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request, + FIRGetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback(nil, [FIRAuthErrorUtils tooManyRequestsErrorWithMessage:nil]); + }); + }); + + FIRAuthCredential *linkEmailCredential = + [FIREmailAuthProvider credentialWithEmail:kEmail password:kPassword]; + [authResult.user linkAndRetrieveDataWithCredential:linkEmailCredential + completion:^(FIRAuthDataResult *_Nullable + linkAuthResult, + NSError *_Nullable error) { + XCTAssertNil(linkAuthResult); + XCTAssertEqual(error.code, FIRAuthErrorCodeTooManyRequests); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testlinkCredentialSuccess + @brief Tests the flow of a successful @c linkWithCredential:completion: call, without additional + IDP data. + */ +- (void)testlinkCredentialSuccess { + [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID + federatedID:kFacebookID + displayName:kFacebookDisplayName + profile:[[self class] googleProfile] + providerIDToken:kFacebookIDToken + providerAccessToken:kFacebookAccessToken]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *facebookCredential = + [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken]; + [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserFacebook:authResult.user]; + XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]); + XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName); + XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID); + XCTAssertNil(error); + + [self expectVerifyAssertionRequest:FIRGoogleAuthProviderID + federatedID:kGoogleID + displayName:kGoogleDisplayName + profile:[[self class] googleProfile] + providerIDToken:kGoogleIDToken + providerAccessToken:kGoogleAccessToken]; + + FIRAuthCredential *linkGoogleCredential = + [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken]; + [authResult.user linkWithCredential:linkGoogleCredential + completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertNil(error); + id<FIRUserInfo> userInfo = user.providerData.firstObject; + XCTAssertEqual(userInfo.providerID, FIRGoogleAuthProviderID); + XCTAssertEqual([FIRAuth auth].currentUser, authResult.user); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUserGoogle:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testlinkCredentialError + @brief Tests the flow of an unsuccessful @c linkWithCredential:completion: call, with an error + from the backend. + */ +- (void)testlinkCredentialError { + [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID + federatedID:kFacebookID + displayName:kFacebookDisplayName + profile:[[self class] googleProfile] + providerIDToken:kFacebookIDToken + providerAccessToken:kFacebookAccessToken]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *facebookCredential = + [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken]; + [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserFacebook:authResult.user]; + XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]); + XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName); + XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID); + XCTAssertNil(error); + + OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request, + FIRVerifyAssertionResponseCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback(nil, [FIRAuthErrorUtils userDisabledErrorWithMessage:nil]); + }); + }); + + FIRAuthCredential *linkGoogleCredential = + [FIRGoogleAuthProvider credentialWithIDToken:kGoogleIDToken accessToken:kGoogleAccessToken]; + [authResult.user linkWithCredential:linkGoogleCredential + completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertNil(user); + XCTAssertEqual(error.code, FIRAuthErrorCodeUserDisabled); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testlinkCredentialProviderAlreadyLinkedError + @brief Tests the flow of an unsuccessful @c linkWithCredential:completion: call, with a client + side error. + */ +- (void)testlinkCredentialProviderAlreadyLinkedError { + [self expectVerifyAssertionRequest:FIRFacebookAuthProviderID + federatedID:kFacebookID + displayName:kFacebookDisplayName + profile:[[self class] googleProfile] + providerIDToken:kFacebookIDToken + providerAccessToken:kFacebookAccessToken]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + FIRAuthCredential *facebookCredential = + [FIRFacebookAuthProvider credentialWithAccessToken:kFacebookAccessToken]; + [[FIRAuth auth] signInAndRetrieveDataWithCredential:facebookCredential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserFacebook:authResult.user]; + XCTAssertEqualObjects(authResult.additionalUserInfo.profile, [[self class] googleProfile]); + XCTAssertEqualObjects(authResult.additionalUserInfo.username, kUserName); + XCTAssertEqualObjects(authResult.additionalUserInfo.providerID, FIRFacebookAuthProviderID); + XCTAssertNil(error); + + FIRAuthCredential *linkFacebookCredential = + [FIRFacebookAuthProvider credentialWithAccessToken:kGoogleAccessToken]; + [authResult.user linkWithCredential:linkFacebookCredential + completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertNil(user); + XCTAssertEqual(error.code, FIRAuthErrorCodeProviderAlreadyLinked); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testlinkPhoneAuthCredentialSuccess + @brief Tests the flow of a successful @c linkAndRetrieveDataWithCredential:completion: + call using a phoneAuthCredential. + */ +- (void)testlinkPhoneAuthCredentialSuccess { + id (^mockUserInfoWithPhoneNumber)(NSString *) = ^(NSString *phoneNumber) { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + if (phoneNumber.length) { + NSDictionary *userInfoDictionary = @{ @"providerId" : FIRPhoneAuthProviderID }; + FIRGetAccountInfoResponseProviderUserInfo *userInfo = + [[FIRGetAccountInfoResponseProviderUserInfo alloc] initWithDictionary:userInfoDictionary]; + OCMStub([mockGetAccountInfoResponseUser providerUserInfo]).andReturn(@[ userInfo ]); + OCMStub([mockGetAccountInfoResponseUser phoneNumber]).andReturn(phoneNumber); + } + return mockGetAccountInfoResponseUser; + }; + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id userInfoResponse = mockUserInfoWithPhoneNumber(nil); + [self signInWithEmailPasswordWithMockUserInfoResponse:userInfoResponse + completion:^(FIRUser *user) { + [self expectVerifyPhoneNumberRequestWithPhoneNumber:kPhoneNumber error:nil]; + id userInfoResponseUpdate = mockUserInfoWithPhoneNumber(kPhoneNumber); + [self expectGetAccountInfoWithMockUserInfoResponse:userInfoResponseUpdate]; + + FIRPhoneAuthCredential *credential = + [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID + verificationCode:kVerificationCode]; + [user linkAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult *_Nullable + linkAuthResult, + NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects([FIRAuth auth].currentUser.providerData.firstObject.providerID, + FIRPhoneAuthProviderID); + XCTAssertEqualObjects([FIRAuth auth].currentUser.phoneNumber, kPhoneNumber); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testUnlinkPhoneAuthCredentialSuccess + @brief Tests the flow of a successful @c unlinkFromProvider:completion: call using a + @c FIRPhoneAuthProvider. + */ +- (void)testUnlinkPhoneAuthCredentialSuccess { + id (^mockUserInfoWithPhoneNumber)(NSString *) = ^(NSString *phoneNumber) { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + if (phoneNumber.length) { + NSDictionary *userInfoDictionary = @{ @"providerId" : FIRPhoneAuthProviderID }; + FIRGetAccountInfoResponseProviderUserInfo *userInfo = + [[FIRGetAccountInfoResponseProviderUserInfo alloc] initWithDictionary:userInfoDictionary]; + OCMStub([mockGetAccountInfoResponseUser providerUserInfo]).andReturn(@[ userInfo ]); + OCMStub([mockGetAccountInfoResponseUser phoneNumber]).andReturn(phoneNumber); + } + return mockGetAccountInfoResponseUser; + }; + + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request, + FIRSetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + XCTAssertNotNil(request.deleteProviders); + XCTAssertNil(request.email); + XCTAssertNil(request.localID); + XCTAssertNil(request.displayName); + XCTAssertNil(request.photoURL); + XCTAssertNil(request.password); + XCTAssertNil(request.providers); + XCTAssertNil(request.deleteAttributes); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]); + callback(mockSetAccountInfoResponse, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id userInfoResponse = mockUserInfoWithPhoneNumber(nil); + [self signInWithEmailPasswordWithMockUserInfoResponse:userInfoResponse + completion:^(FIRUser *user) { + [self expectVerifyPhoneNumberRequestWithPhoneNumber:kPhoneNumber error:nil]; + id userInfoResponseUpdate = mockUserInfoWithPhoneNumber(kPhoneNumber); + [self expectGetAccountInfoWithMockUserInfoResponse:userInfoResponseUpdate]; + + FIRPhoneAuthCredential *credential = + [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID + verificationCode:kVerificationCode]; + // Link phone credential. + [user linkAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult *_Nullable + linkAuthResult, + NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects([FIRAuth auth].currentUser.providerData.firstObject.providerID, + FIRPhoneAuthProviderID); + XCTAssertEqualObjects([FIRAuth auth].currentUser.phoneNumber, kPhoneNumber); + // Immediately unlink the phone auth provider. + [user unlinkFromProvider:FIRPhoneAuthProviderID + completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertNil([FIRAuth auth].currentUser.phoneNumber); + [expectation fulfill]; + }]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testlinkPhoneAuthCredentialFailure + @brief Tests the flow of a failed call to @c linkAndRetrieveDataWithCredential:completion: due + to a phone provider already being linked. + */ +- (void)testlinkPhoneAuthCredentialFailure { + id (^mockUserInfoWithPhoneNumber)(NSString *) = ^(NSString *phoneNumber) { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + if (phoneNumber.length) { + OCMStub([mockGetAccountInfoResponseUser phoneNumber]).andReturn(phoneNumber); + } + return mockGetAccountInfoResponseUser; + }; + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id userInfoResponse = mockUserInfoWithPhoneNumber(nil); + [self signInWithEmailPasswordWithMockUserInfoResponse:userInfoResponse + completion:^(FIRUser *user) { + NSError *error = [FIRAuthErrorUtils providerAlreadyLinkedError]; + [self expectVerifyPhoneNumberRequestWithPhoneNumber:nil error:error]; + FIRPhoneAuthCredential *credential = + [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID + verificationCode:kVerificationCode]; + [user linkAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult *_Nullable + linkAuthResult, + NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqual(error.code, FIRAuthErrorCodeProviderAlreadyLinked); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testlinkPhoneCredentialAlreadyExistsError + @brief Tests the flow of @c linkAndRetrieveDataWithCredential:completion: + call using a phoneAuthCredential and a credential already exisits error. In this case we + should get a FIRAuthCredential in the error object. + */ +- (void)testlinkPhoneCredentialAlreadyExistsError { + id (^mockUserInfoWithPhoneNumber)(NSString *) = ^(NSString *phoneNumber) { + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail); + OCMStub([mockGetAccountInfoResponseUser passwordHash]).andReturn(kPasswordHash); + if (phoneNumber.length) { + OCMStub([mockGetAccountInfoResponseUser phoneNumber]).andReturn(phoneNumber); + } + return mockGetAccountInfoResponseUser; + }; + + void (^expectVerifyPhoneNumberRequest)(NSString *) = ^(NSString *phoneNumber) { + OCMExpect([_mockBackend verifyPhoneNumber:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyPhoneNumberRequest *_Nullable request, + FIRVerifyPhoneNumberResponseCallback callback) { + XCTAssertEqualObjects(request.verificationID, kVerificationID); + XCTAssertEqualObjects(request.verificationCode, kVerificationCode); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + FIRPhoneAuthCredential *credential = + [[FIRPhoneAuthCredential alloc] initWithTemporaryProof:kTemporaryProof + phoneNumber:kPhoneNumber + providerID:FIRPhoneAuthProviderID]; + callback(nil, + [FIRAuthErrorUtils credentialAlreadyInUseErrorWithMessage:nil + credential:credential]); + }); + }); + }; + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id userInfoResponse = mockUserInfoWithPhoneNumber(nil); + [self signInWithEmailPasswordWithMockUserInfoResponse:userInfoResponse + completion:^(FIRUser *user) { + expectVerifyPhoneNumberRequest(kPhoneNumber); + + FIRPhoneAuthCredential *credential = + [[FIRPhoneAuthProvider provider] credentialWithVerificationID:kVerificationID + verificationCode:kVerificationCode]; + [user linkAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult *_Nullable + linkAuthResult, + NSError *_Nullable error) { + XCTAssertNil(linkAuthResult); + XCTAssertEqual(error.code, FIRAuthErrorCodeCredentialAlreadyInUse); + FIRPhoneAuthCredential *credential = error.userInfo[FIRAuthUpdatedCredentialKey]; + XCTAssertEqual(credential.temporaryProof, kTemporaryProof); + XCTAssertEqual(credential.phoneNumber, kPhoneNumber); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +#pragma mark - Helpers + +/** @fn signInWithEmailPasswordWithMockGetAccountInfoResponse:completion: + @brief Signs in with an email and password account with mocked backend end calls. + @param mockUserInfoResponse A mocked FIRGetAccountInfoResponseUser object. + @param completion The completion block that takes the newly signed-in user as the only + parameter. + */ +- (void)signInWithEmailPasswordWithMockUserInfoResponse:(id)mockUserInfoResponse + completion:(void (^)(FIRUser *user))completion { + OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request, + FIRVerifyPasswordResponseCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVeriyPasswordResponse = OCMClassMock([FIRVerifyPasswordResponse class]); + OCMStub([mockVeriyPasswordResponse IDToken]).andReturn(kAccessToken); + OCMStub([mockVeriyPasswordResponse approximateExpirationDate]) + .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]); + OCMStub([mockVeriyPasswordResponse refreshToken]).andReturn(kRefreshToken); + callback(mockVeriyPasswordResponse, nil); + }); + }); + [self expectGetAccountInfoWithMockUserInfoResponse:mockUserInfoResponse]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] signInWithEmail:kEmail password:kPassword completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertNotNil(user); + XCTAssertNil(error); + completion(user); + }]; +} + +/** @fn expectGetAccountInfoWithMockUserInfoResponse: + @brief Expects a GetAccountInfo request on the mock backend and calls back with provided + fake account data. + @param mockUserInfoResponse A mock @c FIRGetAccountInfoResponseUser object containing user info. + */ +- (void)expectGetAccountInfoWithMockUserInfoResponse:(id)mockUserInfoResponse { + OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request, + FIRGetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]); + OCMStub([mockGetAccountInfoResponse users]).andReturn(@[ mockUserInfoResponse ]); + callback(mockGetAccountInfoResponse, nil); + }); + }); +} + +/** @fn dictionaryWithUserInfoArray: + @brief Converts an array of @c FIRUserInfo into a dictionary that indexed by provider IDs. + @param userInfoArray An array of @c FIRUserInfo objects. + @return A dictionary contains same values as @c userInfoArray does but keyed by their + @c providerID . + */ +- (NSDictionary<NSString *, id<FIRUserInfo>> *) + dictionaryWithUserInfoArray:(NSArray<id<FIRUserInfo>> *)userInfoArray { + NSMutableDictionary<NSString *, id<FIRUserInfo>> *map = + [NSMutableDictionary dictionaryWithCapacity:userInfoArray.count]; + for (id<FIRUserInfo> userInfo in userInfoArray) { + XCTAssertNil(map[userInfo.providerID]); + map[userInfo.providerID] = userInfo; + } + return map; +} + +/** @fn stubSecureTokensWithMockResponse + @brief Creates stubs on the mock response object with access and refresh tokens + @param mockResponse The mock response object. + */ +- (void)stubTokensWithMockResponse:(id)mockResponse { + OCMStub([mockResponse IDToken]).andReturn(kAccessToken); + OCMStub([mockResponse approximateExpirationDate]) + .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]); + OCMStub([mockResponse refreshToken]).andReturn(kRefreshToken); +} + +/** @fn assertUserGoogle + @brief Asserts the given FIRUser matching the fake data returned by + @c expectGetAccountInfo:federatedID:displayName: . + @param user The user object to be verified. + */ +- (void)assertUserGoogle:(FIRUser *)user { + XCTAssertNotNil(user); + XCTAssertEqualObjects(user.uid, kLocalID); + XCTAssertEqualObjects(user.displayName, kGoogleDisplayName); + XCTAssertEqual(user.providerData.count, 1u); + id<FIRUserInfo> googleUserInfo = user.providerData[0]; + XCTAssertEqualObjects(googleUserInfo.providerID, FIRGoogleAuthProviderID); + XCTAssertEqualObjects(googleUserInfo.uid, kGoogleID); + XCTAssertEqualObjects(googleUserInfo.displayName, kGoogleDisplayName); + XCTAssertEqualObjects(googleUserInfo.email, kGoogleEmail); +} + +/** @fn assertUserFacebook + @brief Asserts the given FIRUser matching the fake data returned by + @c expectGetAccountInfo:federatedID:displayName: . + @param user The user object to be verified. + */ +- (void)assertUserFacebook:(FIRUser *)user { + XCTAssertNotNil(user); + XCTAssertEqualObjects(user.uid, kLocalID); + XCTAssertEqualObjects(user.displayName, kFacebookDisplayName); + XCTAssertEqual(user.providerData.count, 1u); + id<FIRUserInfo> googleUserInfo = user.providerData[0]; + XCTAssertEqualObjects(googleUserInfo.providerID, FIRFacebookAuthProviderID); + XCTAssertEqualObjects(googleUserInfo.uid, kFacebookID); + XCTAssertEqualObjects(googleUserInfo.displayName, kFacebookDisplayName); + XCTAssertEqualObjects(googleUserInfo.email, kGoogleEmail); +} + +/** @fn expectGetAccountInfo:federatedID:displayName: + @brief Expects a GetAccountInfo request on the mock backend and calls back with fake account + data for a Google Sign-In user. + */ +- (void)expectGetAccountInfo:(NSString *)providerId + federatedID:(NSString *)federatedID + displayName:(NSString *)displayName { + OCMExpect([_mockBackend getAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetAccountInfoRequest *_Nullable request, + FIRGetAccountInfoResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGoogleUserInfo = OCMClassMock([FIRGetAccountInfoResponseProviderUserInfo class]); + OCMStub([mockGoogleUserInfo providerID]).andReturn(providerId); + OCMStub([mockGoogleUserInfo displayName]).andReturn(displayName); + OCMStub([mockGoogleUserInfo federatedID]).andReturn(federatedID); + OCMStub([mockGoogleUserInfo email]).andReturn(kGoogleEmail); + id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]); + OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID); + OCMStub([mockGetAccountInfoResponseUser displayName]).andReturn(displayName); + OCMStub([mockGetAccountInfoResponseUser providerUserInfo]) + .andReturn((@[ mockGoogleUserInfo ])); + id mockGetAccountInfoResponse = OCMClassMock([FIRGetAccountInfoResponse class]); + OCMStub([mockGetAccountInfoResponse users]).andReturn(@[ mockGetAccountInfoResponseUser ]); + callback(mockGetAccountInfoResponse, nil); + }); + }); +} + +/** @fn expectVerifyAssertionRequest:federatedID:displayName:profile:providerAccessToken: + @brief Expects a Verify Assertion request on the mock backend and calls back with fake account + data. + */ +- (void)expectVerifyAssertionRequest:(NSString *)providerId + federatedID:(NSString *)federatedID + displayName:(NSString *)displayName + profile:(NSDictionary *)profile + providerIDToken:(nullable NSString *)providerIDToken + providerAccessToken:(NSString *)providerAccessToken { + OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request, + FIRVerifyAssertionResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.providerID, providerId); + XCTAssertEqualObjects(request.providerIDToken, providerIDToken); + XCTAssertEqualObjects(request.providerAccessToken, providerAccessToken); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVeriyAssertionResponse = OCMClassMock([FIRVerifyAssertionResponse class]); + OCMStub([mockVeriyAssertionResponse federatedID]).andReturn(federatedID); + OCMStub([mockVeriyAssertionResponse providerID]).andReturn(providerId); + OCMStub([mockVeriyAssertionResponse localID]).andReturn(kLocalID); + OCMStub([mockVeriyAssertionResponse displayName]).andReturn(displayName); + OCMStub([mockVeriyAssertionResponse profile]).andReturn(profile); + OCMStub([mockVeriyAssertionResponse username]).andReturn(kUserName); + [self stubTokensWithMockResponse:mockVeriyAssertionResponse]; + callback(mockVeriyAssertionResponse, nil); + }); + }); + [self expectGetAccountInfo:providerId federatedID:federatedID displayName:displayName]; +} + +/** @fn expectVerifyPhoneNumberRequestWithPhoneNumber:error: + @brief Expects a verify phone numner request on the mock backend and calls back with fake + account data or an error. + @param phoneNumber Optionally; The phone number to use in the mocked response. + @param error Optionally; The error to return in the mocked response. + */ +- (void)expectVerifyPhoneNumberRequestWithPhoneNumber:(nullable NSString *)phoneNumber + error:(nullable NSError*)error { + OCMExpect([_mockBackend verifyPhoneNumber:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyPhoneNumberRequest *_Nullable request, + FIRVerifyPhoneNumberResponseCallback callback) { + XCTAssertEqualObjects(request.verificationID, kVerificationID); + XCTAssertEqualObjects(request.verificationCode, kVerificationCode); + XCTAssertEqualObjects(request.accessToken, kAccessToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + if (error) { + callback(nil, error); + return; + } + id mockVerifyPhoneNumberResponse = OCMClassMock([FIRVerifyPhoneNumberResponse class]); + OCMStub([mockVerifyPhoneNumberResponse phoneNumber]).andReturn(phoneNumber); + callback(mockVerifyPhoneNumberResponse, nil); + }); + }); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Auth/Tests/FIRVerifyAssertionRequestTests.m b/Example/Auth/Tests/FIRVerifyAssertionRequestTests.m new file mode 100644 index 0000000..becc420 --- /dev/null +++ b/Example/Auth/Tests/FIRVerifyAssertionRequestTests.m @@ -0,0 +1,232 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRGetOOBConfirmationCodeResponse.h" +#import "FIRVerifyAssertionRequest.h" +#import "FIRVerifyAssertionResponse.h" +#import "FIRFakeBackendRPCIssuer.h" +#import <GoogleToolboxForMac/GTMNSDictionary+URLArguments.h> + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kTestPostBodyKey + @brief The name of the "postBody" property in the response. + */ +static NSString *const kPostBodyKey = @"postBody"; + +/** @var kExpectedAPIURL + @brief The expected URL for test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAssertion?key=APIKey"; + +/** @var kIDTokenKey + @brief The name of the "idToken" property in the response. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** @var kTestAccessToken + @brief Fake access token used for testing. + */ +static NSString *const kTestAccessToken = @"ACCESS_TOKEN"; + +/** @var kProviderIDKey + @brief The key for the "providerId" value in the request. + */ +static NSString *const kProviderIDKey = @"providerId"; + +/** @var kTestProviderID + @brief Fake provider ID used for testing. + */ +static NSString *const kTestProviderID = @"ProviderID"; + +/** @var kProviderIDTokenKey + @brief The key for the "id_token" value in the request. + */ +static NSString *const kProviderIDTokenKey = @"id_token"; + +/** @var kTestProviderIDToken + @brief Fake provider ID token used for testing. + */ +static NSString *const kTestProviderIDToken = @"ProviderIDToken"; + +/** @var kInputEmailKey + @brief The key for the "inputEmail" value in the request. + */ +static NSString *const kInputEmailKey = @"identifier"; + +/** @var kTestInputEmail + @brief Fake input email used for testing. + */ +static NSString *const kTestInputEmail = @"testInputEmail"; + +/** @var kPendingIDTokenKey + @brief The key for the "pendingIdToken" value in the request. + */ +static NSString *const kPendingIDTokenKey = @"pendingIdToken"; + +/** @var kTestPendingToken + @brief Fake pending token used for testing. + */ +static NSString *const kTestPendingToken = @"testPendingToken"; + +/** @var kProviderAccessTokenKey + @brief The key for the "access_token" value in the request. + */ +static NSString *const kProviderAccessTokenKey = @"access_token"; + +/** @var kTestProviderAccessToken + @brief Fake @c providerAccessToken used for testing the request. + */ +static NSString *const kTestProviderAccessToken = @"testProviderAccessToken"; + +/** @var kProviderOAuthTokenSecretKey + @brief The key for the "oauth_token_secret" value in the request. + */ +static NSString *const kProviderOAuthTokenSecretKey = @"oauth_token_secret"; + +/** @var kTestProviderOAuthTokenSecret + @brief Fake @c providerOAuthTokenSecret used for testing the request. + */ +static NSString *const kTestProviderOAuthTokenSecret = @"testProviderOAuthTokenSecret"; + +/** @var kReturnSecureTokenKey + @brief The key for the "returnSecureToken" value in the request. + */ +static NSString *const kReturnSecureTokenKey = @"returnSecureToken"; + +/** @var kAutoCreateKey + @brief The key for the "auto-create" value in the request. + */ +static NSString *const kAutoCreateKey = @"autoCreate"; + +/** @class FIRVerifyAssertionRequestTests + @brief Tests for @c FIRVerifyAssertionReuqest + */ +@interface FIRVerifyAssertionRequestTests : XCTestCase +@end +@implementation FIRVerifyAssertionRequestTests{ + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testVerifyAssertionRequestMissingTokens + @brief Tests the request with missing @c providerAccessToken and @c provideIDToken. + @remarks The request creation will raise an @c NSInvalidArgumentException exception when both + these tokens are missing. + */ +- (void)testVerifyAssertionRequestMissingTokens { + FIRVerifyAssertionRequest *request = + [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID]; + + FIRVerifyAssertionResponseCallback callback = + ^(FIRVerifyAssertionResponse *_Nullable response, NSError *_Nullable error) {}; + void (^verifyAssertionBlock)(void) = ^{ + [FIRAuthBackend verifyAssertion:request callback:callback]; + }; + XCTAssertThrowsSpecificNamed(verifyAssertionBlock(), NSException, NSInvalidArgumentException, + @"Either IDToken or accessToken must be supplied."); + XCTAssertNil(_RPCIssuer.decodedRequest[kPostBodyKey]); +} + +/** @fn testVerifyAssertionRequestProviderAccessToken + @brief Tests the verify assertion request with the @c providerAccessToken field set. + @remarks The presence of the @c providerAccessToken will prevent an @c + NSInvalidArgumentException exception from being raised. + */ +- (void)testVerifyAssertionRequestProviderAccessToken { + FIRVerifyAssertionRequest *request = + [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID]; + request.providerAccessToken = kTestProviderAccessToken; + request.returnSecureToken = NO; + [FIRAuthBackend verifyAssertion:request + callback:^(FIRVerifyAssertionResponse *_Nullable response, + NSError *_Nullable error) { + }]; + + NSDictionary *postBody = @{ + kProviderIDKey : kTestProviderID, + kProviderAccessTokenKey : kTestProviderAccessToken + }; + NSString *postBodyArgs = [postBody gtm_httpArgumentsString]; + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest[kPostBodyKey]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPostBodyKey], postBodyArgs); + XCTAssertNil(_RPCIssuer.decodedRequest[kIDTokenKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kReturnSecureTokenKey]); + // Auto-create flag Should be true by default. + XCTAssertTrue([_RPCIssuer.decodedRequest[kAutoCreateKey] boolValue]); +} + +/** @fn testVerifyAssertionRequestOptionalFields + @brief Tests the verify assertion request with all optinal fields set. + */ +- (void)testVerifyAssertionRequestOptionalFields { + FIRVerifyAssertionRequest *request = + [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID]; + request.providerIDToken = kTestProviderIDToken; + request.providerAccessToken = kTestProviderAccessToken; + request.accessToken = kTestAccessToken; + request.inputEmail = kTestInputEmail; + request.pendingIDToken = kTestPendingToken; + request.providerOAuthTokenSecret = kTestProviderOAuthTokenSecret; + request.autoCreate = NO; + + [FIRAuthBackend verifyAssertion:request + callback:^(FIRVerifyAssertionResponse *_Nullable response, + NSError *_Nullable error) { + }]; + + NSDictionary *postBody = @{ + kProviderIDKey : kTestProviderID, + kProviderIDTokenKey : kTestProviderIDToken, + kProviderAccessTokenKey : kTestProviderAccessToken, + kProviderOAuthTokenSecretKey : kTestProviderOAuthTokenSecret, + kInputEmailKey : kTestInputEmail + }; + NSString *postBodyArgs = [postBody gtm_httpArgumentsString]; + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest[kPostBodyKey]); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPostBodyKey], postBodyArgs); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIDTokenKey], kTestAccessToken); + XCTAssertTrue([_RPCIssuer.decodedRequest[kReturnSecureTokenKey] boolValue]); + XCTAssertFalse([_RPCIssuer.decodedRequest[kAutoCreateKey] boolValue]); +} + +@end diff --git a/Example/Auth/Tests/FIRVerifyAssertionResponseTests.m b/Example/Auth/Tests/FIRVerifyAssertionResponseTests.m new file mode 100644 index 0000000..cd9e771 --- /dev/null +++ b/Example/Auth/Tests/FIRVerifyAssertionResponseTests.m @@ -0,0 +1,426 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRGetOOBConfirmationCodeResponse.h" +#import "FIRVerifyAssertionRequest.h" +#import "FIRVerifyAssertionResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kProviderIDKey + @brief The name of the "providerId" property in the response. + */ +static NSString *const kProviderIDKey = @"providerId"; + +/** @var kIDTokenKey + @brief The name of the "IDToken" property in the response. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** @var kExpiresInKey + @brief The name of the "expiresIn" property in the response. + */ +static NSString *const kExpiresInKey = @"expiresIn"; + +/** @var kRefreshTokenKey + @brief The name of the "refreshToken" property in the response. + */ +static NSString *const kRefreshTokenKey = @"refreshToken"; + +/** @var kVerifiedProviderKey + @brief The name of the "VerifiedProvider" property in the response. + */ +static NSString *const kVerifiedProviderKey = @"verifiedProvider"; + +/** @var kRawUserInfoKey + @brief The name of the "rawUserInfo" property in the response. + */ +static NSString *const kRawUserInfoKey = @"rawUserInfo"; + +/** @var kUsernameKey + @brief The name of the "username" property in the response. + */ +static NSString *const kUsernameKey = @"username"; + +/** @var kIsNewUserKey + @brief The name of the "isNewUser" property in the response. + */ +static NSString *const kIsNewUserKey = @"isNewUser"; + +/** @var kTestProviderID + @brief Fake provider ID used for testing. + */ +static NSString *const kTestProviderID = @"ProviderID"; + +/** @var kTestProviderIDToken + @brief Fake provider ID token used for testing. + */ +static NSString *const kTestProviderIDToken = @"ProviderIDToken"; + +/** @var kTestIDToken + @brief Testing ID token for verifying assertion. + */ +static NSString *const kTestIDToken = @"ID_TOKEN"; + +/** @var kTestExpiresIn + @brief Fake token expiration time. + */ +static NSString *const kTestExpiresIn = @"12345"; + +/** @var kTestRefreshToken + @brief Fake refresh token. + */ +static NSString *const kTestRefreshToken = @"REFRESH_TOKEN"; + +/** @var kTestProvider + @brief Fake provider used for testing. + */ +static NSString *const kTestProvider = @"Provider"; + +/** @var kPhotoUrlKey + @brief The name of the "PhotoUrl" property in the response. + */ +static NSString *const kPhotoUrlKey = @"photoUrl"; + +/** @var kTestPhotoUrl + @brief The "PhotoUrl" value for testing the response. + */ +static NSString *const kTestPhotoUrl = @"www.example.com"; + +/** @var kUsername + @brief The "username" value for testing the response. + */ +static NSString *const kUsername = @"Joe Doe"; + +/** @var testInvalidCredentialError + @brief This is the error message the server will respond with if the IDP token or requestUri is + invalid. + */ +static NSString *const ktestInvalidCredentialError = @"INVALID_IDP_RESPONSE"; + +/** @var kUserDisabledErrorMessage + @brief This is the error message the server will respond with if the user's account has been + disabled. + */ +static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED"; + +/** @var kOperationNotAllowedErrorMessage + @brief This is the error message the server will respond with if Admin disables IDP specified by + provider. + */ +static NSString *const kOperationNotAllowedErrorMessage = @"OPERATION_NOT_ALLOWED"; + +/** @var kPasswordLoginDisabledErrorMessage + @brief This is the error message the server responds with if password login is disabled. + */ +static NSString *const kPasswordLoginDisabledErrorMessage = @"PASSWORD_LOGIN_DISABLED"; + +/** @var kFederatedUserIDAlreadyLinkedMessage + @brief This is the error message the server will respond with if the federated user ID has been + already linked with another account. + */ +static NSString *const kFederatedUserIDAlreadyLinkedMessage = @"FEDERATED_USER_ID_ALREADY_LINKED:"; + +/** @var kEpsilon + @brief Allowed difference when comparing floating point numbers. + */ +static const double kEpsilon = 1e-3; + +/** @class FIRVerifyAssertionResponseTests + @brief Tests for @c FIRVerifyAssertionResponse + */ +@interface FIRVerifyAssertionResponseTests : XCTestCase +@end +@implementation FIRVerifyAssertionResponseTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +/** @fn profile + @brief The "rawUserInfo" value for testing the response. + */ ++ (NSDictionary *)profile { + static NSDictionary *kGoogleProfile = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + kGoogleProfile = @{ + @"iss": @"https://accounts.google.com\\", + @"email": @"test@email.com", + @"given_name": @"User", + @"family_name": @"Doe" + }; + }); + return kGoogleProfile; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testInvalidIDPResponseError + @brief This test simulates @c invalidIDPResponseError with @c FIRAuthErrorCodeInvalidIDPResponse + error code. + */ +- (void)testInvalidIDPResponseError { + FIRVerifyAssertionRequest *request = + [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID]; + request.providerIDToken = kTestProviderIDToken; + + __block BOOL callbackInvoked; + __block FIRVerifyAssertionResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyAssertion:request + callback:^(FIRVerifyAssertionResponse*_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:ktestInvalidCredentialError]; + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidCredential); +} + +/** @fn testUserDisabledError + @brief This test simulates @c userDisabledError with @c + FIRAuthErrorCodeUserDisabled error code. + */ +- (void)testUserDisabledError { + FIRVerifyAssertionRequest *request = + [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID]; + request.providerIDToken = kTestProviderIDToken; + + __block BOOL callbackInvoked; + __block FIRVerifyAssertionResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyAssertion:request + callback:^(FIRVerifyAssertionResponse*_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kUserDisabledErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserDisabled); +} + +/** @fn testCredentialAlreadyInUseError + @brief This test simulates a @c FIRAuthErrorCodeCredentialAlreadyInUse error. + */ +- (void)testCredentialAlreadyInUseError { + FIRVerifyAssertionRequest *request = + [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID]; + request.providerIDToken = kTestProviderIDToken; + + __block BOOL callbackInvoked; + __block FIRVerifyAssertionResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyAssertion:request + callback:^(FIRVerifyAssertionResponse*_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kFederatedUserIDAlreadyLinkedMessage]; + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeCredentialAlreadyInUse); +} + +/** @fn testOperationNotAllowedError + @brief This test simulates a @c FIRAuthErrorCodeOperationNotAllowed error. + */ +- (void)testOperationNotAllowedError { + FIRVerifyAssertionRequest *request = + [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID]; + request.providerIDToken = kTestProviderIDToken; + + __block BOOL callbackInvoked; + __block FIRVerifyAssertionResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyAssertion:request + callback:^(FIRVerifyAssertionResponse*_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kOperationNotAllowedErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed); +} + +/** @fn testPasswordLoginDisabledError + @brief This test simulates a @c FIRAuthErrorCodeOperationNotAllowed error. + */ +- (void)testPasswordLoginDisabledError { + FIRVerifyAssertionRequest *request = + [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID]; + request.providerIDToken = kTestProviderIDToken; + + __block BOOL callbackInvoked; + __block FIRVerifyAssertionResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyAssertion:request + callback:^(FIRVerifyAssertionResponse*_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kPasswordLoginDisabledErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed); +} + +/** @fn testSuccessfulVerifyAssertionResponse + @brief This test simulates a successful verify assertion flow. + */ +- (void)testSuccessfulVerifyAssertionResponse { + FIRVerifyAssertionRequest *request = + [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID]; + request.providerIDToken = kTestProviderIDToken; + + __block BOOL callbackInvoked; + __block FIRVerifyAssertionResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyAssertion:request + callback:^(FIRVerifyAssertionResponse*_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + kProviderIDKey : kTestProviderID, + kIDTokenKey : kTestIDToken, + kExpiresInKey : kTestExpiresIn, + kRefreshTokenKey : kTestRefreshToken, + kVerifiedProviderKey : @[ kTestProvider ], + kPhotoUrlKey : kTestPhotoUrl, + kUsernameKey : kUsername, + kIsNewUserKey : @YES, + kRawUserInfoKey : [[self class] profile] + }]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.IDToken, kTestIDToken); + NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow]; + XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon); + XCTAssertEqualObjects(RPCResponse.refreshToken, kTestRefreshToken); + XCTAssertEqualObjects(RPCResponse.verifiedProvider, @[ kTestProvider ]); + XCTAssertEqualObjects(RPCResponse.photoURL, [NSURL URLWithString:kTestPhotoUrl]); + XCTAssertEqualObjects(RPCResponse.username, kUsername); + XCTAssertEqualObjects(RPCResponse.profile, [[self class] profile]); + XCTAssertEqualObjects(RPCResponse.providerID, kTestProviderID); + XCTAssertTrue(RPCResponse.isNewUser); +} + +/** @fn testSuccessfulVerifyAssertionResponseWithTextData + @brief This test simulates a successful verify assertion flow when response collection + fields are sent as text values. + */ +- (void)testSuccessfulVerifyAssertionResponseWithTextData { + FIRVerifyAssertionRequest *request = + [[FIRVerifyAssertionRequest alloc] initWithAPIKey:kTestAPIKey providerID:kTestProviderID]; + request.providerIDToken = kTestProviderIDToken; + + __block BOOL callbackInvoked; + __block FIRVerifyAssertionResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyAssertion:request + callback:^(FIRVerifyAssertionResponse*_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + + [_RPCIssuer respondWithJSON:@{ + kProviderIDKey : kTestProviderID, + kIDTokenKey : kTestIDToken, + kExpiresInKey : kTestExpiresIn, + kRefreshTokenKey : kTestRefreshToken, + kVerifiedProviderKey : [[self class] convertToJSONString:@[ kTestProvider ]], + kPhotoUrlKey : kTestPhotoUrl, + kUsernameKey : kUsername, + kIsNewUserKey : @NO, + kRawUserInfoKey : [[self class] convertToJSONString:[[self class] profile]] + }]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.IDToken, kTestIDToken); + NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow]; + XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon); + XCTAssertEqualObjects(RPCResponse.refreshToken, kTestRefreshToken); + XCTAssertEqualObjects(RPCResponse.verifiedProvider, @[ kTestProvider ]); + XCTAssertEqualObjects(RPCResponse.photoURL, [NSURL URLWithString:kTestPhotoUrl]); + XCTAssertEqualObjects(RPCResponse.username, kUsername); + XCTAssertEqualObjects(RPCResponse.profile, [[self class] profile]); + XCTAssertEqualObjects(RPCResponse.providerID, kTestProviderID); + XCTAssertFalse(RPCResponse.isNewUser); +} + +#pragma mark - Helpers + ++ (NSString *)convertToJSONString:(NSObject *)object { + NSData *objectAsData = [NSJSONSerialization dataWithJSONObject:object + options:0 + error:nil]; + return [[NSString alloc] initWithData:objectAsData encoding:NSUTF8StringEncoding]; +} + +@end diff --git a/Example/Auth/Tests/FIRVerifyClientRequestTest.m b/Example/Auth/Tests/FIRVerifyClientRequestTest.m new file mode 100644 index 0000000..dcb00f6 --- /dev/null +++ b/Example/Auth/Tests/FIRVerifyClientRequestTest.m @@ -0,0 +1,94 @@ +/* + * Copyright 2017 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 "FIRAuthBackend.h" +#import "FIRVerifyClientRequest.h" +#import "FIRVerifyClientResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kFakeAppToken + @brief The fake app token to use in the test request. + */ +static NSString *const kFakeAppToken = @"appToken"; + +/** @var kFakeAPIKey + @brief The fake API key to use in the test request. + */ +static NSString *const kFakeAPIKey = @"APIKey"; + +/** @var kAppTokenKey + @brief The key for the appToken request paramenter. + */ +static NSString *const kAPPTokenKey = @"appToken"; + +/** @var kIsSandboxKey + @brief The key for the isSandbox request parameter + */ +static NSString *const kIsSandboxKey = @"isSandbox"; + +/** @var kExpectedAPIURL + @brief The expected URL for the test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyClient?key=APIKey"; + +/** @class FIRVerifyClientRequestTest + @brief Tests for @c FIRVerifyClientRequests. + */ +@interface FIRVerifyClientRequestTest : XCTestCase +@end + +@implementation FIRVerifyClientRequestTest { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testVerifyClientRequest + @brief Tests the verify client request. + */ +- (void)testVerifyClientRequest { + FIRVerifyClientRequest *request = + [[FIRVerifyClientRequest alloc] initWithAppToken:kFakeAppToken + isSandbox:YES + APIKey:kFakeAPIKey]; + [FIRAuthBackend verifyClient:request callback:^(FIRVerifyClientResponse *_Nullable response, + NSError *_Nullable error) { + }]; + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kAPPTokenKey], kFakeAppToken); + XCTAssertTrue(_RPCIssuer.decodedRequest[kIsSandboxKey]); +} + +@end diff --git a/Example/Auth/Tests/FIRVerifyClientResponseTests.m b/Example/Auth/Tests/FIRVerifyClientResponseTests.m new file mode 100644 index 0000000..68b8feb --- /dev/null +++ b/Example/Auth/Tests/FIRVerifyClientResponseTests.m @@ -0,0 +1,178 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRVerifyClientRequest.h" +#import "FIRVerifyClientResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kFakeAppToken + @brief The fake app token to use in the test request. + */ +static NSString *const kFakeAppToken = @"appToken"; + +/** @var kFakeAPIKey + @brief The fake API key to use in the test request. + */ +static NSString *const kFakeAPIKey = @"APIKey"; + +/** @var kAppTokenKey + @brief The key for the appToken request paramenter. + */ +static NSString *const kAPPTokenKey = @"appToken"; + +/** @var kIsSandboxKey + @brief The key for the isSandbox request parameter + */ +static NSString *const kIsSandboxKey = @"isSandbox"; + +/** @var kReceiptKey + @brief The key for the receipt response paramenter. + */ +static NSString *const kReceiptKey = @"receipt"; + +/** @var kFakeReceipt + @brief The fake receipt returned in the response. + */ +static NSString *const kFakeReceipt = @"receipt"; + +/** @var kSuggestedTimeOutKey + @brief The key for the suggested timeout response parameter + */ +static NSString *const kSuggestedTimeOutKey = @"suggestedTimeout"; + +/** @var kFakeSuggestedTimeout + @brief The fake suggested timeout returned in the response. + */ +static NSString *const kFakeSuggestedTimeout = @"1234"; + +/** @var kEpsilon + @brief Allowed difference when comparing floating point numbers. + */ +static const double kEpsilon = 1e-3; + +/** @var kMissingAppCredentialErrorMessage + @brief This is the error message the server will respond with if the APNS token is missing in a + verifyClient request is missing. + */ +static NSString *const kMissingAppCredentialErrorMessage = @"MISSING_APP_CREDENTIAL"; + +/** @var kMissingAppCredentialErrorMessage + @brief This is the error message the server will respond with if the APNS token is missing in a + verifyClient request is invalid. + */ +static NSString *const kInvalidAppCredentialErrorMessage = @"INVALID_APP_CREDENTIAL"; + +@interface FIRVerifyClientResponseTests : XCTestCase +@end + +@implementation FIRVerifyClientResponseTests{ + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +/** @fn testMissingAppCredentialError + @brief Tests that @c FIRAuthErrorCodeMissingAppCredential error. + */ +- (void)testMissingAppCredentialError { + FIRVerifyClientRequest *request = + [[FIRVerifyClientRequest alloc] initWithAppToken:kFakeAppToken + isSandbox:YES + APIKey:kFakeAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyClientResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyClient:request + callback:^(FIRVerifyClientResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kMissingAppCredentialErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeMissingAppCredential); +} + +/** @fn testInvalidAppCredentialError + @brief Tests that @c FIRAuthErrorCodeInvalidAppCredential error. + */ +- (void)testInvalidAppCredentialError { + FIRVerifyClientRequest *request = + [[FIRVerifyClientRequest alloc] initWithAppToken:kFakeAppToken + isSandbox:YES + APIKey:kFakeAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyClientResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyClient:request + callback:^(FIRVerifyClientResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kInvalidAppCredentialErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidAppCredential); +} + +/** @fn testSuccessfulVerifyClientResponse + @brief Tests a succesful attempt of the verify password flow. + */ +- (void)testSuccessfulVerifyPasswordResponse { + FIRVerifyClientRequest *request = + [[FIRVerifyClientRequest alloc] initWithAppToken:kFakeAppToken + isSandbox:YES + APIKey:kFakeAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyClientResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyClient:request + callback:^(FIRVerifyClientResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + [_RPCIssuer respondWithJSON:@{ + kReceiptKey : kFakeReceipt, + kSuggestedTimeOutKey : kFakeSuggestedTimeout + }]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.receipt, kFakeReceipt); + NSTimeInterval suggestedTimeout = [RPCResponse.suggestedTimeOutDate timeIntervalSinceNow]; + XCTAssertLessThanOrEqual(fabs(suggestedTimeout - [kFakeSuggestedTimeout doubleValue]), kEpsilon); +} + +@end diff --git a/Example/Auth/Tests/FIRVerifyCustomTokenRequestTests.m b/Example/Auth/Tests/FIRVerifyCustomTokenRequestTests.m new file mode 100644 index 0000000..9f65f73 --- /dev/null +++ b/Example/Auth/Tests/FIRVerifyCustomTokenRequestTests.m @@ -0,0 +1,110 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRGetOOBConfirmationCodeResponse.h" +#import "FIRVerifyCustomTokenRequest.h" +#import "FIRVerifyCustomTokenResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kTestTokenKey + @brief The name of the "token" property in the response. + */ +static NSString *const kTestTokenKey = @"token"; + +/** @var kTestToken + @brief testing token. + */ +static NSString *const kTestToken = @"test token"; + +/** @var kReturnSecureTokenKey + @brief The key for the "returnSecureToken" value in the request. + */ +static NSString *const kReturnSecureTokenKey = @"returnSecureToken"; + +/** @var kExpectedAPIURL + @brief The expected URL for test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=APIKey"; + +@interface FIRVerifyCustomTokenRequestTests : XCTestCase +@end +@implementation FIRVerifyCustomTokenRequestTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testVerifyCustomTokenRequest + @brief Tests the verify custom token request. + */ +- (void)testVerifyCustomTokenRequest { + FIRVerifyCustomTokenRequest *request = + [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey]; + request.returnSecureToken = NO; + [FIRAuthBackend verifyCustomToken:request + callback:^(FIRVerifyCustomTokenResponse *_Nullable response, + NSError *_Nullable error) { + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertNotNil(_RPCIssuer.decodedRequest[kTestTokenKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kReturnSecureTokenKey]); +} + +/** @fn testVerifyCustomTokenRequestOptionalFields + @brief Tests the verify custom token request with optional fields. + */ +- (void)testVerifyCustomTokenRequestOptionalFields { + FIRVerifyCustomTokenRequest *request = + [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey]; + [FIRAuthBackend verifyCustomToken:request + callback:^(FIRVerifyCustomTokenResponse *_Nullable response, + NSError *_Nullable error) { + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertNotNil(_RPCIssuer.decodedRequest[kTestTokenKey]); + XCTAssertTrue([_RPCIssuer.decodedRequest[kReturnSecureTokenKey] boolValue]); +} + +@end diff --git a/Example/Auth/Tests/FIRVerifyCustomTokenResponseTests.m b/Example/Auth/Tests/FIRVerifyCustomTokenResponseTests.m new file mode 100644 index 0000000..7a634ed --- /dev/null +++ b/Example/Auth/Tests/FIRVerifyCustomTokenResponseTests.m @@ -0,0 +1,274 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRVerifyCustomTokenRequest.h" +#import "FIRVerifyCustomTokenResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestToken + @brief testing token. + */ +static NSString *const kTestToken = @"test token"; + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kIDTokenKey + @brief The name of the "IDToken" property in the response. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** @var kExpiresInKey + @brief The name of the "expiresIn" property in the response. + */ +static NSString *const kExpiresInKey = @"expiresIn"; + +/** @var kRefreshTokenKey + @brief The name of the "refreshToken" property in the response. + */ +static NSString *const kRefreshTokenKey = @"refreshToken"; + +/** @var kTestIDToken + @brief Testing ID token for verifying assertion. + */ +static NSString *const kTestIDToken = @"ID_TOKEN"; + +/** @var kTestExpiresIn + @brief Fake token expiration time. + */ +static NSString *const kTestExpiresIn = @"12345"; + +/** @var kTestRefreshToken + @brief Fake refresh token. + */ +static NSString *const kTestRefreshToken = @"REFRESH_TOKEN"; + +/** @var kMissingTokenCustomErrorMessage + @brief This is the error message the server will respond with if token field is missing in + request. + */ +static NSString *const kMissingCustomTokenErrorMessage = @"MISSING_CUSTOM_TOKEN"; + +/** @var kInvalidTokenCustomErrorMessage + @brief This is the error message the server will respond with if there is a validation error + with the custom token. + */ +static NSString *const kInvalidCustomTokenErrorMessage = @"INVALID_CUSTOM_TOKEN"; + +/** @var kInvalidCustomTokenServerErrorMessage + @brief This is the error message the server will respond with if there is a validation error + with the custom token. This message contains error details from the server. + */ +static NSString *const kInvalidCustomTokenServerErrorMessage = + @"INVALID_CUSTOM_TOKEN : Detailed Error"; + +/** @var kInvalidCustomTokenEmptyServerErrorMessage + @brief This is the error message the server will respond with if there is a validation error + with the custom token. + @remarks This message deliberately has no content where it should contain + error details. + */ +static NSString *const kInvalidCustomTokenEmptyServerErrorMessage = + @"INVALID_CUSTOM_TOKEN :"; + +/** @var kInvalidCustomTokenErrorDetails + @brief This is the test detailed error message that could be returned by the backend. + */ +static NSString *const kInvalidCustomTokenErrorDetails = @"Detailed Error"; + +/** @var kCredentialMismatchErrorMessage + @brief This is the error message the server will respond with if the service API key belongs to + different projects. + */ +static NSString *const kCredentialMismatchErrorMessage = @"CREDENTIAL_MISMATCH:"; + +/** @var kEpsilon + @brief Allowed difference when comparing floating point numbers. + */ +static const double kEpsilon = 1e-3; + +@interface FIRVerifyCustomTokenResponseTests : XCTestCase +@end +@implementation FIRVerifyCustomTokenResponseTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testInvalidCustomTokenError + @brief This test simulates @c invalidCustomTokenError with @c + FIRAuthErrorCodeINvalidCustomToken error code. + */ +- (void)testInvalidCustomTokenError { + FIRVerifyCustomTokenRequest *request = + [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRVerifyCustomTokenResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyCustomToken:request + callback:^(FIRVerifyCustomTokenResponse*_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kInvalidCustomTokenErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidCustomToken); +} + +/** @fn testInvalidCustomTokenServerError + @brief This test simulates @c invalidCustomTokenError with @c + FIRAuthErrorCodeINvalidCustomToken error code, with a custom message from the server. + */ +- (void)testInvalidCustomTokenServerError { + FIRVerifyCustomTokenRequest *request = + [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRVerifyCustomTokenResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyCustomToken:request + callback:^(FIRVerifyCustomTokenResponse*_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kInvalidCustomTokenServerErrorMessage]; + NSString *errorDescription = [RPCError.userInfo valueForKey:NSLocalizedDescriptionKey]; + XCTAssertTrue([errorDescription isEqualToString:kInvalidCustomTokenErrorDetails]); + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidCustomToken); +} + +/** @fn testEmptyServerDetailMessage + @brief This test simulates @c invalidCustomTokenError with @c + FIRAuthErrorCodeINvalidCustomToken error code, with an empty custom message from the server. + @remarks An empty error message is not valid and therefore should not be added as an error + description. + */ +- (void)testEmptyServerDetailMessage { + FIRVerifyCustomTokenRequest *request = + [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRVerifyCustomTokenResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyCustomToken:request + callback:^(FIRVerifyCustomTokenResponse*_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kInvalidCustomTokenEmptyServerErrorMessage]; + NSString *errorDescription = [RPCError.userInfo valueForKey:NSLocalizedDescriptionKey]; + XCTAssertFalse([errorDescription isEqualToString:@""]); + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidCustomToken); +} + +/** @fn testInvalidCredentialMismatchError + @brief This test simulates @c credentialMistmatchTokenError with @c + FIRAuthErrorCodeCredetialMismatch error code. + */ +- (void)testInvalidCredentialMismatchError { + FIRVerifyCustomTokenRequest *request = + [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRVerifyCustomTokenResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyCustomToken:request + callback:^(FIRVerifyCustomTokenResponse*_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kCredentialMismatchErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeCustomTokenMismatch); +} + +/** @fn testSuccessfulVerifyCustomTokenResponse + @brief This test simulates a successful @c VerifyCustomToken flow. + */ +- (void)testSuccessfulVerifyCustomTokenResponse { + FIRVerifyCustomTokenRequest *request = + [[FIRVerifyCustomTokenRequest alloc] initWithToken:kTestToken APIKey:kTestAPIKey]; + + __block BOOL callbackInvoked; + __block FIRVerifyCustomTokenResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyCustomToken:request + callback:^(FIRVerifyCustomTokenResponse*_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithJSON:@{ + kIDTokenKey : kTestIDToken, + kExpiresInKey : kTestExpiresIn, + kRefreshTokenKey : kTestRefreshToken, + }]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCError); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.IDToken, kTestIDToken); + NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow]; + XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon); + XCTAssertEqualObjects(RPCResponse.refreshToken, kTestRefreshToken); +} + +@end diff --git a/Example/Auth/Tests/FIRVerifyPasswordRequestTest.m b/Example/Auth/Tests/FIRVerifyPasswordRequestTest.m new file mode 100644 index 0000000..f07afdc --- /dev/null +++ b/Example/Auth/Tests/FIRVerifyPasswordRequestTest.m @@ -0,0 +1,163 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRVerifyPasswordRequest.h" +#import "FIRVerifyPasswordResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kEmailKey + @brief The key for the "email" value in the request. + */ +static NSString *const kEmailKey = @"email"; + +/** @var kPasswordKey + @brief The key for the "password" value in the request. + */ +static NSString *const kPasswordKey = @"password"; + +/** @var kTestEmail + @brief Fake email address for testing the request. + */ +static NSString *const kTestEmail = @"testEmail."; + +/** @var kTestPassword + @brief Fake password for testing the request. + */ +static NSString *const kTestPassword = @"testPassword"; + +/** @var kPendingIDTokenKey + @brief The key for the "pendingIdToken" value in the request. + */ +static NSString *const kPendingIDTokenKey = @"pendingIdToken"; + +/** @var kTestPendingToken + @brief Fake pendingToken for testing the request. + */ +static NSString *const kTestPendingToken = @"testPendingToken"; + +/** @var kCaptchaChallengeKey + @brief The key for the "captchaChallenge" value in the request. + */ +static NSString *const kCaptchaChallengeKey = @"captchaChallenge"; + +/** @var kTestCaptchaChallenge + @brief Fake captchaChallenge for testing the request. + */ +static NSString *const kTestCaptchaChallenge = @"testCaptchaChallenge"; + +/** @var kCaptchaResponseKey + @brief The key for the "captchaResponse" value in the request. + */ +static NSString *const kCaptchaResponseKey = @"captchaResponse"; + +/** @var kTestCaptchaResponse + @brief Fake captchaResponse for testing the request. + */ +static NSString *const kTestCaptchaResponse = @"captchaResponse"; + +/** @var kReturnSecureTokenKey + @brief The key for the "returnSecureToken" value in the request. + */ +static NSString *const kReturnSecureTokenKey = @"returnSecureToken"; + +/** @var kExpectedAPIURL + @brief The expected URL for test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=APIKey"; + +/** @class FIRVerifyPasswordRequestTest + @brief Tests for @c FIRVerifyPasswordRequestTest. + */ +@interface FIRVerifyPasswordRequestTest : XCTestCase +@end +@implementation FIRVerifyPasswordRequestTest { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testVerifyPasswordRequest + @brief Tests the verify password request. + */ +- (void)testVerifyPasswordRequest { + FIRVerifyPasswordRequest * request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail + password:kTestPassword + APIKey:kTestAPIKey]; + request.returnSecureToken = NO; + [FIRAuthBackend verifyPassword:request + callback:^(FIRVerifyPasswordResponse *_Nullable response, + NSError *_Nullable error) { + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kEmailKey], kTestEmail); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPasswordKey], kTestPassword); + XCTAssertNil(_RPCIssuer.decodedRequest[kCaptchaChallengeKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kCaptchaResponseKey]); + XCTAssertNil(_RPCIssuer.decodedRequest[kReturnSecureTokenKey]); +} + +/** @fn testVerifyPasswordRequestOptionalFields + @brief Tests the verify password request with optional fields. + */ +- (void)testVerifyPasswordRequestOptionalFields { + FIRVerifyPasswordRequest * request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail + password:kTestPassword + APIKey:kTestAPIKey]; + request.pendingIDToken = kTestPendingToken; + request.captchaChallenge = kTestCaptchaChallenge; + request.captchaResponse = kTestCaptchaResponse; + [FIRAuthBackend verifyPassword:request + callback:^(FIRVerifyPasswordResponse *_Nullable response, + NSError *_Nullable error) { + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kEmailKey], kTestEmail); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPasswordKey], kTestPassword); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kCaptchaChallengeKey], kTestCaptchaChallenge); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kCaptchaResponseKey], kTestCaptchaResponse); + XCTAssertTrue([_RPCIssuer.decodedRequest[kReturnSecureTokenKey] boolValue]); +} + +@end diff --git a/Example/Auth/Tests/FIRVerifyPasswordResponseTests.m b/Example/Auth/Tests/FIRVerifyPasswordResponseTests.m new file mode 100644 index 0000000..949969b --- /dev/null +++ b/Example/Auth/Tests/FIRVerifyPasswordResponseTests.m @@ -0,0 +1,454 @@ +/* + * Copyright 2017 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 "FIRAuthErrors.h" +#import "FIRAuthBackend.h" +#import "FIRVerifyPasswordRequest.h" +#import "FIRVerifyPasswordResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestPassword + @brief Testing user password. + */ +static NSString *const kTestPassword = @"testpassword"; + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"_test_API_key_"; + +/** @var kLocalIDKey + @brief The name of the 'localID' property in the response. + */ +static NSString *const kLocalIDKey = @"localId"; + +/** @var kTestLocalID + @brief The fake localID for testing the response. + */ +static NSString *const kTestLocalID = @"testLocalId"; + +/** @var kEmailKey + @brief The name of the 'email' property in the response. + */ +static NSString *const kEmailKey = @"email"; + +/** @var kTestEmail + @brief Fake user email for testing the response. + */ +static NSString *const kTestEmail = @"test@gmail.com"; + +/** @var kDisplayNameKey + @brief The name of the 'displayName' property in the response. + */ +static NSString *const kDisplayNameKey = @"displayName"; + +/** @var kTestDisplayName + @brief Fake displayName for testing the response. + */ +static NSString *const kTestDisplayName = @"testDisplayName"; + +/** @var kIDTokenKey + @brief The name of the "IDToken" property in the response. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** @var kTestIDToken + @brief Testing ID token for verifying assertion. + */ +static NSString *const kTestIDToken = @"ID_TOKEN"; + +/** @var kExpiresInKey + @brief The name of the "expiresIn" property in the response. + */ +static NSString *const kExpiresInKey = @"expiresIn"; + +/** @var kTestExpiresIn + @brief Fake token expiration time. + */ +static NSString *const kTestExpiresIn = @"12345"; + +/** @var kRefreshTokenKey + @brief The name of the "refreshToken" property in the response. + */ +static NSString *const kRefreshTokenKey = @"refreshToken"; + +/** @var kTestRefreshToken + @brief Fake refresh token. + */ +static NSString *const kTestRefreshToken = @"REFRESH_TOKEN"; + +/** @var kOperationNotAllowedErrorMessage + @brief This is the error message the server will respond with if Admin disables IDP specified by + provider. + */ +static NSString *const kOperationNotAllowedErrorMessage = @"OPERATION_NOT_ALLOWED"; + +/** @var kPasswordLoginDisabledErrorMessage + @brief This is the error message the server responds with if password login is disabled. + */ +static NSString *const kPasswordLoginDisabledErrorMessage = @"PASSWORD_LOGIN_DISABLED"; + +/** @var kPhotoUrlKey + @brief The name of the 'photoUrl' property in the response. + */ +static NSString *const kPhotoUrlKey = @"photoUrl"; + +/** @var kTestPhotoUrl + @brief Fake photoUrl for testing the response. + */ +static NSString *const kTestPhotoUrl = @"www.example.com"; + +/** @var kUserDisabledErrorMessage + @brief This is the error message the server will respond with if the user's account has been + disabled. + */ +static NSString *const kUserDisabledErrorMessage = @"USER_DISABLED"; + +/** @var kEmailNotFoundErrorMessage + @brief This is the error message the server will respond with if the email entered is not + found. + */ +static NSString *const kEmailNotFoundErrorMessage = @"EMAIL_NOT_FOUND"; + +/** @var kWrongPasswordErrorMessage + @brief This is the error message the server will respond with if the user entered a wrong + password. + */ +static NSString *const kWrongPasswordErrorMessage = @"INVALID_PASSWORD"; + +/** @var kInvalidEmailErrorMessage + @brief The error returned by the server if the email is invalid. + */ +static NSString *const kInvalidEmailErrorMessage = @"INVALID_EMAIL"; + +/** @var kBadRequestErrorMessage + @brief This is the error message returned when a bad request is made; often due to a bad API + Key. + */ +static NSString *const kBadRequestErrorMessage = @"Bad Request"; + +/** @var kInvalidKeyReasonValue + @brief The value for the "reason" key indicating an invalid API Key was received by the server. + */ +static NSString *const kInvalidKeyReasonValue = @"keyInvalid"; + +/** @var kAppNotAuthorizedReasonValue + @brief The value for the "reason" key indicating the App is not authorized to use Firebase + Authentication. + */ +static NSString *const kAppNotAuthorizedReasonValue = @"ipRefererBlocked"; + +/** @var kTooManyAttemptsErrorMessage + @brief This is the error message the server will respond with if a user has tried (and failed) + to sign in too many times. + */ +static NSString *const kTooManyAttemptsErrorMessage = @"TOO_MANY_ATTEMPTS_TRY_LATER:"; + +/** @var kEpsilon + @brief Allowed difference when comparing floating point numbers. + */ +static const double kEpsilon = 1e-3; + +/** @class FIRVerifyPasswordResponseTests + @brief Tests for @c FIRVerifyPasswordResponse. + */ +@interface FIRVerifyPasswordResponseTests : XCTestCase +@end +@implementation FIRVerifyPasswordResponseTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +/** @fn testUserDisabledError + @brief Tests that @c FIRAuthErrorCodeUserDisabled error is received if the email is disabled. + */ +- (void)testUserDisabledError { + FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail + password:kTestPassword + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyPassword:request + callback:^(FIRVerifyPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kUserDisabledErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserDisabled); +} + +/** @fn testEmailNotFoundError + @brief Tests that @c FIRAuthErrorCodeEmailNotFound error is received if the email is not found. + */ +- (void)testEmailNotFoundError { + FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail + password:kTestPassword + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyPassword:request + callback:^(FIRVerifyPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kEmailNotFoundErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserNotFound); +} + +/** @fn testInvalidPasswordError + @brief Tests that @c FIRAuthErrorCodeInvalidPassword error is received if the password is + invalid. + */ +- (void)testInvalidPasswordError { + FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail + password:kTestPassword + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyPassword:request + callback:^(FIRVerifyPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kWrongPasswordErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeWrongPassword); +} + +/** @fn testInvalidEmailError + @brief Tests that @c FIRAuthErrorCodeInvalidEmail error is received if the email address has an + incorrect format. + */ +- (void)testInvalidEmailError { + FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail + password:kTestPassword + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyPassword:request + callback:^(FIRVerifyPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kInvalidEmailErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidEmail); +} + +/** @fn testTooManyAttemptsError + @brief Tests that @c FIRAuthErrorCodeTooManyRequests error is received if too many sign-in + attempts were made. + */ +- (void)testTooManySignInAttemptsError { + FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail + password:kTestPassword + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyPassword:request + callback:^(FIRVerifyPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + [_RPCIssuer respondWithServerErrorMessage:kTooManyAttemptsErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeTooManyRequests); +} + +/** @fn testKeyInvalid + @brief Tests that @c FIRAuthErrorCodeInvalidApiKey error is received from the server. + */ +- (void)testKeyInvalid { + FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail + password:kTestPassword + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyPassword:request + callback:^(FIRVerifyPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + NSDictionary *errorDictionary = @{ + @"error" : @{ + @"message" : kBadRequestErrorMessage, + @"errors" : @[ @{ @"reason" : kInvalidKeyReasonValue } ] + } + }; + [_RPCIssuer respondWithJSONError:errorDictionary]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidAPIKey); +} + +/** @fn testOperationNotAllowedError + @brief This test simulates a @c FIRAuthErrorCodeOperationNotAllowed error. + */ +- (void)testOperationNotAllowedError { + FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail + password:kTestPassword + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyPassword:request + callback:^(FIRVerifyPasswordResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kOperationNotAllowedErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed); +} + +/** @fn testPasswordLoginDisabledError + @brief This test simulates a @c FIRAuthErrorCodeOperationNotAllowed error. + */ +- (void)testPasswordLoginDisabledError { + FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail + password:kTestPassword + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyPassword:request + callback:^(FIRVerifyPasswordResponse *_Nullable response, + NSError *_Nullable error) { + callbackInvoked = YES; + RPCResponse = response; + RPCError = error; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kPasswordLoginDisabledErrorMessage]; + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCError); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeOperationNotAllowed); +} + +/** @fn testAppNotAuthorized + @brief Tests that @c FIRAuthErrorCodeAppNotAuthorized error is received from the server. + */ +- (void)testAppNotAuthorized { + FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail + password:kTestPassword + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyPassword:request + callback:^(FIRVerifyPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + NSDictionary *errorDictionary = @{ + @"error" : @{ + @"message" : kBadRequestErrorMessage, + @"errors" : @[ @{ @"reason" : kAppNotAuthorizedReasonValue } ] + } + }; + [_RPCIssuer respondWithJSONError:errorDictionary]; + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeAppNotAuthorized); +} + +/** @fn testSuccessfulVerifyPasswordResponse + @brief Tests a succesful attempt of the verify password flow. + */ +- (void)testSuccessfulVerifyPasswordResponse { + FIRVerifyPasswordRequest *request = [[FIRVerifyPasswordRequest alloc] initWithEmail:kTestEmail + password:kTestPassword + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPasswordResponse *RPCResponse; + __block NSError *RPCError; + [FIRAuthBackend verifyPassword:request + callback:^(FIRVerifyPasswordResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + [_RPCIssuer respondWithJSON:@{ + kLocalIDKey : kTestLocalID, + kEmailKey : kTestEmail, + kDisplayNameKey : kTestDisplayName, + kIDTokenKey : kTestIDToken, + kExpiresInKey : kTestExpiresIn, + kRefreshTokenKey : kTestRefreshToken, + kPhotoUrlKey : kTestPhotoUrl + }]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.email, kTestEmail); + XCTAssertEqualObjects(RPCResponse.localID, kTestLocalID); + XCTAssertEqualObjects(RPCResponse.displayName, kTestDisplayName); + XCTAssertEqualObjects(RPCResponse.IDToken, kTestIDToken); + NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow]; + XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon); + XCTAssertEqualObjects(RPCResponse.refreshToken, kTestRefreshToken); + XCTAssertEqualObjects(RPCResponse.photoURL.absoluteString, kTestPhotoUrl ); +} + +@end diff --git a/Example/Auth/Tests/FIRVerifyPhoneNumberRequestTests.m b/Example/Auth/Tests/FIRVerifyPhoneNumberRequestTests.m new file mode 100644 index 0000000..f51d102 --- /dev/null +++ b/Example/Auth/Tests/FIRVerifyPhoneNumberRequestTests.m @@ -0,0 +1,154 @@ +/* + * Copyright 2017 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 "FIRAuthBackend.h" +#import "FIRVerifyPhoneNumberRequest.h" +#import "FIRVerifyPhoneNumberResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kVerificationCode + @brief Fake verification code used for testing. + */ +static NSString *const kVerificationCode = @"12345678"; + +/** @var kVerificationID + @brief Fake verification ID for testing. + */ +static NSString *const kVerificationID = @"55432"; + +/** @var kPhoneNumber + @brief The fake user phone number. + */ +static NSString *const kPhoneNumber = @"12345658"; + +/** @var kTemporaryProof + @brief The fake temporary proof. + */ +static NSString *const kTemporaryProof = @"12345658"; + +/** @var kVerificationCodeKey + @brief The key for the verification code" value in the request. + */ +static NSString *const kVerificationCodeKey = @"code"; + +/** @var kVerificationIDKey + @brief The key for the verification ID" value in the request. + */ +static NSString *const kVerificationIDKey = @"sessionInfo"; + +/** @var kIDTokenKey + @brief The key for the "ID Token" value in the request. + */ +static NSString *const kIDTokenKey = @"idToken"; + +/** @var kTestAccessToken + @bried Fake acess token for testing. + */ + static NSString *const kTestAccessToken = @"accessToken"; + + /** @var kTemporaryProofKey + @brief The key for the temporary proof value in the request. + */ +static NSString *const kTemporaryProofKey = @"temporaryProof"; + +/** @var kPhoneNumberKey + @brief The key for the phone number value in the request. + */ +static NSString *const kPhoneNumberKey = @"phoneNumber"; + +/** @var kExpectedAPIURL + @brief The expected URL for the test calls. + */ +static NSString *const kExpectedAPIURL = + @"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhoneNumber?key=APIKey"; + +/** @class FIRVerifyPhoneNumberRequestTests + @brief Tests for @c FIRVerifyPhoneNumberRequest. + */ +@interface FIRVerifyPhoneNumberRequestTests : XCTestCase +@end + +@implementation FIRVerifyPhoneNumberRequestTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + [super setUp]; + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testVerifyPhoneNumberRequest + @brief Tests the verifyPhoneNumber request. + */ +- (void)testVerifyPhoneNumberRequest { + FIRVerifyPhoneNumberRequest *request = + [[FIRVerifyPhoneNumberRequest alloc] initWithVerificationID:kVerificationID + verificationCode:kVerificationCode + APIKey:kTestAPIKey]; + request.accessToken = kTestAccessToken; + [FIRAuthBackend verifyPhoneNumber:request + callback:^(FIRVerifyPhoneNumberResponse *_Nullable response, + NSError *_Nullable error) { + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kVerificationIDKey], kVerificationID); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kVerificationCodeKey], kVerificationCode); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIDTokenKey], kTestAccessToken); +} + +/** @fn testVerifyPhoneNumberRequestWithTemporaryProof + @brief Tests the verifyPhoneNumber request when created using a temporary proof. + */ +- (void)testVerifyPhoneNumberRequestWithTemporaryProof { + FIRVerifyPhoneNumberRequest *request = + [[FIRVerifyPhoneNumberRequest alloc] initWithTemporaryProof:kTemporaryProof + phoneNumber:kPhoneNumber + APIKey:kTestAPIKey]; + request.accessToken = kTestAccessToken; + [FIRAuthBackend verifyPhoneNumber:request + callback:^(FIRVerifyPhoneNumberResponse *_Nullable response, + NSError *_Nullable error) { + }]; + + XCTAssertEqualObjects(_RPCIssuer.requestURL.absoluteString, kExpectedAPIURL); + XCTAssertNotNil(_RPCIssuer.decodedRequest); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kTemporaryProofKey], kTemporaryProof); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kPhoneNumberKey], kPhoneNumber); + XCTAssertEqualObjects(_RPCIssuer.decodedRequest[kIDTokenKey], kTestAccessToken); +} + +@end diff --git a/Example/Auth/Tests/FIRVerifyPhoneNumberResponseTests.m b/Example/Auth/Tests/FIRVerifyPhoneNumberResponseTests.m new file mode 100644 index 0000000..c647c3d --- /dev/null +++ b/Example/Auth/Tests/FIRVerifyPhoneNumberResponseTests.m @@ -0,0 +1,271 @@ +/* + * Copyright 2017 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 "Phone/FIRPhoneAuthCredential_Internal.h" +#import "FIRAuthBackend.h" +#import "FIRAuthErrors.h" +#import "FIRVerifyPhoneNumberRequest.h" +#import "FIRVerifyPhoneNumberResponse.h" +#import "FIRFakeBackendRPCIssuer.h" + +/** @var kTestAPIKey + @brief Fake API key used for testing. + */ +static NSString *const kTestAPIKey = @"APIKey"; + +/** @var kVerificationCode + @brief Fake verification code used for testing. + */ +static NSString *const kVerificationCode = @"12345678"; + +/** @var kVerificationID + @brief Fake verification ID for testing. + */ +static NSString *const kVerificationID = @"55432"; + +/** @var kfakeRefreshToken + @brief Fake refresh token for testing. + */ +static NSString *const kfakeRefreshToken = @"refreshtoken"; + +/** @var klocalID + @brief Fake local ID for testing. + */ +static NSString *const klocalID = @"localID"; + +/** @var kfakeIDToken + @brief Fake ID Token for testing. + */ +static NSString *const kfakeIDToken = @"idtoken"; + +/** @var kTestExpiresIn + @brief Fake token expiration time. + */ +static NSString *const kTestExpiresIn = @"12345"; + +/** @var kInvalidVerificationCodeErrorMessage + @brief This is the error message the server will respond with if an invalid verification code + provided. + */ +static NSString *const kInvalidVerificationCodeErrorMessage = @"INVALID_CODE"; + +/** @var kInvalidSessionInfoErrorMessage + @brief This is the error message the server will respond with if an invalid verification ID + provided. + */ +static NSString *const kInvalidSessionInfoErrorMessage = @"INVALID_SESSION_INFO"; + +/** @var kSessionExpiredErrorMessage + @brief This is the error message the server will respond with if the SMS code has expired before + it is used. + */ +static NSString *const kSessionExpiredErrorMessage = @"SESSION_EXPIRED"; + +/** @var kFakePhoneNumber + @brief The fake user phone number. + */ +static NSString *const kFakePhoneNumber = @"12345658"; + +/** @var kFakeTemporaryProof + @brief The fake temporary proof. + */ +static NSString *const kFakeTemporaryProof = @"12345658"; + +/** @var kEpsilon + @brief Allowed difference when comparing floating point numbers. + */ +static const double kEpsilon = 1e-3; + +/** @class FIRVerifyPhoneNumberResponseTests + @brief Tests for @c FIRVerifyPhoneNumberResponse. + */ +@interface FIRVerifyPhoneNumberResponseTests : XCTestCase + +@end + +@implementation FIRVerifyPhoneNumberResponseTests { + /** @var _RPCIssuer + @brief This backend RPC issuer is used to fake network responses for each test in the suite. + In the @c setUp method we initialize this and set @c FIRAuthBackend's RPC issuer to it. + */ + FIRFakeBackendRPCIssuer *_RPCIssuer; +} + +- (void)setUp { + FIRFakeBackendRPCIssuer *RPCIssuer = [[FIRFakeBackendRPCIssuer alloc] init]; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:RPCIssuer]; + _RPCIssuer = RPCIssuer; +} + +- (void)tearDown { + _RPCIssuer = nil; + [FIRAuthBackend setDefaultBackendImplementationWithRPCIssuer:nil]; + [super tearDown]; +} + +/** @fn testInvalidVerificationCodeError + @brief Tests invalid verification code error. + */ +- (void)testInvalidVerificationCodeError { + FIRVerifyPhoneNumberRequest *request = + [[FIRVerifyPhoneNumberRequest alloc] initWithVerificationID:kVerificationID + verificationCode:kVerificationCode + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPhoneNumberResponse *RPCResponse; + __block NSError *RPCError; + + + [FIRAuthBackend verifyPhoneNumber:request + callback:^(FIRVerifyPhoneNumberResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kInvalidVerificationCodeErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidVerificationCode); +} + +/** @fn testInvalidVerificationIDError + @brief Tests invalid verification code error. + */ +- (void)testInvalidVerificationIDError { + FIRVerifyPhoneNumberRequest *request = + [[FIRVerifyPhoneNumberRequest alloc] initWithVerificationID:kVerificationID + verificationCode:kVerificationCode + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPhoneNumberResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend verifyPhoneNumber:request + callback:^(FIRVerifyPhoneNumberResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kInvalidSessionInfoErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeInvalidVerificationID); +} + +/** @fn testSessionExpiredError + @brief Tests session expired error code. + */ +- (void)testSessionExpiredError { + FIRVerifyPhoneNumberRequest *request = + [[FIRVerifyPhoneNumberRequest alloc] initWithVerificationID:kVerificationID + verificationCode:kVerificationCode + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPhoneNumberResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend verifyPhoneNumber:request + callback:^(FIRVerifyPhoneNumberResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + [_RPCIssuer respondWithServerErrorMessage:kSessionExpiredErrorMessage]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + XCTAssertEqual(RPCError.code, FIRAuthErrorCodeSessionExpired); +} + +/** @fn testSuccessfulVerifyPhoneNumberResponse + @brief Tests a succesful to verify phone number flow. + */ +- (void)testSuccessfulVerifyPhoneNumberResponse { + FIRVerifyPhoneNumberRequest *request = + [[FIRVerifyPhoneNumberRequest alloc] initWithVerificationID:kVerificationID + verificationCode:kVerificationCode + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPhoneNumberResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend verifyPhoneNumber:request + callback:^(FIRVerifyPhoneNumberResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"idToken" : kfakeIDToken, + @"refreshToken" : kfakeRefreshToken, + @"localID" : klocalID, + @"expiresIn" : kTestExpiresIn, + @"isNewUser" : @YES // Set new user flag to true. + }]; + + XCTAssert(callbackInvoked); + XCTAssertNotNil(RPCResponse); + XCTAssertEqualObjects(RPCResponse.IDToken, kfakeIDToken); + XCTAssertEqualObjects(RPCResponse.refreshToken, kfakeRefreshToken); + NSTimeInterval expiresIn = [RPCResponse.approximateExpirationDate timeIntervalSinceNow]; + XCTAssertLessThanOrEqual(fabs(expiresIn - [kTestExpiresIn doubleValue]), kEpsilon); + XCTAssertTrue(RPCResponse.isNewUser); +} + +/** @fn testSuccessfulVerifyPhoneNumberResponseWithTemporaryProof + @brief Tests a succesful to verify phone number flow with temporary proof response. + */ +- (void)testSuccessfulVerifyPhoneNumberResponseWithTemporaryProof { + FIRVerifyPhoneNumberRequest *request = + [[FIRVerifyPhoneNumberRequest alloc] initWithTemporaryProof:kFakeTemporaryProof + phoneNumber:kFakePhoneNumber + APIKey:kTestAPIKey]; + __block BOOL callbackInvoked; + __block FIRVerifyPhoneNumberResponse *RPCResponse; + __block NSError *RPCError; + + [FIRAuthBackend verifyPhoneNumber:request + callback:^(FIRVerifyPhoneNumberResponse *_Nullable response, + NSError *_Nullable error) { + RPCResponse = response; + RPCError = error; + callbackInvoked = YES; + }]; + + [_RPCIssuer respondWithJSON:@{ + @"temporaryProof" : kFakeTemporaryProof, + @"phoneNumber" : kFakePhoneNumber + }]; + + XCTAssert(callbackInvoked); + XCTAssertNil(RPCResponse); + FIRPhoneAuthCredential *credential = RPCError.userInfo[FIRAuthUpdatedCredentialKey]; + XCTAssertEqualObjects(credential.temporaryProof, kFakeTemporaryProof); + XCTAssertEqualObjects(credential.phoneNumber, kFakePhoneNumber); +} + +@end diff --git a/Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.h b/Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.h new file mode 100644 index 0000000..61f9935 --- /dev/null +++ b/Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.h @@ -0,0 +1,112 @@ +/* + * Copyright 2017 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 <Foundation/Foundation.h> + +#import <OCMock/OCMStubRecorder.h> + +NS_ASSUME_NONNULL_BEGIN + +/** @typedef FIRAuthGeneralBlock1 + @brief A general block that takes one id and returns nothing. + */ +typedef void (^FIRAuthGeneralBlock1)(id); + +/** @typedef FIRAuthGeneralBlock2 + @brief A general block that takes two nullable ids and returns nothing. + */ +typedef void (^FIRAuthGeneralBlock2)(id _Nullable, id _Nullable); + +/** @typedef FIRAuthIdDoubleIdBlock + @brief A block that takes third parameters with types @c id, @c double, and @c id . + */ +typedef void (^FIRAuthIdDoubleIdBlock)(id, double, id); + +/** @category OCMStubRecorder(FIRAuthUnitTests) + @brief Utility methods and properties use by Firebase Auth unit tests. + */ +@interface OCMStubRecorder (FIRAuthUnitTests) + +/** @fn andCallBlock1 + @brief Calls a general block that takes one parameter as the action of the stub. + @param block1 A block that takes exactly one 'id'-compatible parameter. + @remarks The method being stubbed must take exactly one parameter, which must be + compatible with type 'id'. + */ +- (id)andCallBlock1:(FIRAuthGeneralBlock1)block1; + +/** @fn andCallBlock2 + @brief Calls a general block that takes two parameters as the action of the stub. + @param block2 A block that takes exactly two 'id'-compatible parameters. + @remarks The method being stubbed must take exactly two parameters, both of which must be + compatible with type 'id'. + */ +- (id)andCallBlock2:(FIRAuthGeneralBlock2)block2; + +/** @fn andDispatchError2 + @brief Dispatchs an error to the second callback parameter in the global auth work queue. + @param error The error to call back as the second argument to the second parameter block. + @remarks The method being stubbed must take exactly two parameters, the first of which must be + compatible with type 'id' and the second of which must be a block that takes an + 'id'-compatible parameter and an NSError* parameter. + */ +- (id)andDispatchError2:(NSError *)error; + +/** @fn andCallIdDoubleIdBlock: + @brief Calls a block that takes three parameters as the action of the stub. + @param block A block that takes exactly three parameters as described. + @remarks The method being stubbed must take exactly three parameters. Its first and the third + parameters must be compatible with type 'id' and its second parameter must be a 'double'. + */ +- (id)andCallIdDoubleIdBlock:(FIRAuthIdDoubleIdBlock)block; + +// This macro allows .andCallBlock1 shorthand to match established style of OCMStubRecorder. +#define andCallBlock1(block1) _andCallBlock1(block1) + +// This macro allows .andCallBlock2 shorthand to match established style of OCMStubRecorder. +#define andCallBlock2(block2) _andCallBlock2(block2) + +// This macro allows .andDispatchError2 shorthand to match established style of OCMStubRecorder. +#define andDispatchError2(block2) _andDispatchError2(block2) + +// This macro allows .andCallIdDoubleIdBlock shorthand to match established style of +// OCMStubRecorder. +#define andCallIdDoubleIdBlock(block) _andCallIdDoubleIdBlock(block) + + +/** @property _andCallBlock1 + @brief A block that calls @c andCallBlock1: method on self. + */ +@property(nonatomic, readonly) OCMStubRecorder *(^ _andCallBlock1)(FIRAuthGeneralBlock1); + +/** @property _andCallBlock2 + @brief A block that calls @c andCallBlock2: method on self. + */ +@property(nonatomic, readonly) OCMStubRecorder *(^ _andCallBlock2)(FIRAuthGeneralBlock2); + +/** @property _andDispatchError2 + @brief A block that calls @c andDispatchError2: method on self. + */ +@property(nonatomic, readonly) OCMStubRecorder *(^ _andDispatchError2)(NSError *); + +/** @property _andCallIdDoubleIdBlock + @brief A block that calls @c andCallBlock2: method on self. + */ +@property(nonatomic, readonly) OCMStubRecorder *(^ _andCallIdDoubleIdBlock)(FIRAuthIdDoubleIdBlock); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.m b/Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.m new file mode 100644 index 0000000..bd43303 --- /dev/null +++ b/Example/Auth/Tests/OCMStubRecorder+FIRAuthUnitTests.m @@ -0,0 +1,103 @@ +/* + * Copyright 2017 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 "OCMStubRecorder+FIRAuthUnitTests.h" + +#import "FIRAuthGlobalWorkQueue.h" + +/** @fn argumentOf + @brief Retrieves a specific argument from a method invocation. + @param invocation The Objective-C method invocation. + @param position The position of the argument to retrieve, starting from 0. + @return The argument at the given position that the method has been invoked with. + @remarks The argument type must be compatible with @c id . + */ +static id argumentOf(NSInvocation *invocation, int position) { + __unsafe_unretained id unretainedArgument; + // Indices 0 and 1 indicate the hidden arguments self and _cmd. Actual arguments starts from 2. + [invocation getArgument:&unretainedArgument atIndex:position + 2]; + // The argument needs to be retained, or it will be released along with the invocation object. + id argument = unretainedArgument; + return argument; +} + +/** @fn doubleArgumentOf + @brief Retrieves a specific argument of type 'double' from a method invocation. + @param invocation The Objective-C method invocation. + @param position The position of the argument to retrieve, starting from 0. + @return The argument at the given position that the method has been invoked with. + @remarks The argument type must be @c double . + */ +static double doubleArgumentOf(NSInvocation *invocation, int position) { + double argument; + // Indices 0 and 1 indicate the hidden arguments self and _cmd. Actual arguments starts from 2. + [invocation getArgument:&argument atIndex:position + 2]; + return argument; +} + +@implementation OCMStubRecorder (FIRAuthUnitTests) + +- (id)andCallBlock1:(FIRAuthGeneralBlock1)block1 { + return [self andDo:^(NSInvocation *invocation) { + block1(argumentOf(invocation, 0)); + }]; +} + +- (id)andCallBlock2:(FIRAuthGeneralBlock2)block2 { + return [self andDo:^(NSInvocation *invocation) { + block2(argumentOf(invocation, 0), argumentOf(invocation, 1)); + }]; +} + +- (id)andDispatchError2:(NSError *)error { + return [self andCallBlock2:^(id request, FIRAuthGeneralBlock2 callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + callback(nil, error); + }); + }]; +} + +- (id)andCallIdDoubleIdBlock:(FIRAuthIdDoubleIdBlock)block { + return [self andDo:^(NSInvocation *invocation) { + block(argumentOf(invocation, 0), doubleArgumentOf(invocation, 2), argumentOf(invocation, 2)); + }]; +} + +- (OCMStubRecorder *(^)(FIRAuthGeneralBlock1))_andCallBlock1 { + return ^(FIRAuthGeneralBlock1 block1) { + return [self andCallBlock1:block1]; + }; +} + +- (OCMStubRecorder *(^)(FIRAuthGeneralBlock2))_andCallBlock2 { + return ^(FIRAuthGeneralBlock2 block2) { + return [self andCallBlock2:block2]; + }; +} + +- (OCMStubRecorder *(^)(NSError *))_andDispatchError2 { + return ^(NSError *error) { + return [self andDispatchError2:error]; + }; +} + +- (OCMStubRecorder *(^)(FIRAuthIdDoubleIdBlock))_andCallIdDoubleIdBlock { + return ^(FIRAuthIdDoubleIdBlock block) { + return [self andCallIdDoubleIdBlock:block]; + }; +} + +@end diff --git a/Example/Auth/Tests/Tests-Info.plist b/Example/Auth/Tests/Tests-Info.plist new file mode 100644 index 0000000..169b6f7 --- /dev/null +++ b/Example/Auth/Tests/Tests-Info.plist @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>BNDL</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1</string> +</dict> +</plist> |