From 5fa4f5aac467db35654c0e3cf6920738cb4b8753 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Tue, 5 Jun 2018 17:11:31 -0700 Subject: Promote users to non-anonymous status even when linking account w/o pw --- Example/Auth/Tests/FIRUserTests.m | 172 +++++++++++++++++++++++++++++++++++++- Firebase/Auth/CHANGELOG.md | 4 + Firebase/Auth/Source/FIRUser.m | 22 +++-- 3 files changed, 191 insertions(+), 7 deletions(-) diff --git a/Example/Auth/Tests/FIRUserTests.m b/Example/Auth/Tests/FIRUserTests.m index 7a6c165..8bb6786 100644 --- a/Example/Auth/Tests/FIRUserTests.m +++ b/Example/Auth/Tests/FIRUserTests.m @@ -26,9 +26,11 @@ #import "FIRAuthGlobalWorkQueue.h" #import "FIRAuthOperationType.h" #import "FIRAuthTokenResult.h" +#import "FIREmailLinkSignInResponse.m" #import "FIRSecureTokenService.h" #import "FIRSecureTokenRequest.h" #import "FIRSecureTokenResponse.h" +#import "FIRSignUpNewUserResponse.h" #import "FIRGetAccountInfoRequest.h" #import "FIRGetAccountInfoResponse.h" #import "FIRSetAccountInfoRequest.h" @@ -583,6 +585,57 @@ static const NSTimeInterval kExpectationTimeout = 2; OCMVerifyAll(_mockBackend); } +/** @fn testUpdateEmailWithAuthLinkAccountSuccess + @brief Tests a successful @c updateEmail:completion: call updates provider info. + */ +- (void)testUpdateEmailWithAuthLinkAccountSuccess { + 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 signInWithEmailLinkWithMockUserInfoResponse: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); + XCTAssertFalse(user.isAnonymous); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + /** @fn testUpdateEmailFailure @brief Tests the flow of a failed @c updateEmail:completion: call. */ @@ -1572,6 +1625,64 @@ static const NSTimeInterval kExpectationTimeout = 2; OCMVerifyAll(_mockBackend); } +/** @fn testLinkingAnonymousAccountsUpdatesIsAnonymous + @brief Tests the flow of a successful @c linkAndRetrieveDataWithCredential:completion: + invocation for email credential. + */ +- (void)testLinkingAnonymousAccountsUpdatesIsAnonymous { + FIRAuthCredential *linkEmailCredential = + [FIREmailAuthProvider credentialWithEmail:kEmail + link:@"https://google.com?oobCode=aCode&mode=signIn"]; + + id (^mockUserInfoWithDisplayName)(NSString *, BOOL) = ^(NSString *displayName, + BOOL hasProviders) { + NSArray *providers = hasProviders ? @[ @{ + @"providerId": FIREmailAuthProviderID, + @"email": kEmail + } ] : @[]; + FIRGetAccountInfoResponseUser *responseUser = + [[FIRGetAccountInfoResponseUser alloc] initWithDictionary:@{ + @"providerUserInfo": providers, + @"localId": kLocalID, + @"displayName": displayName, + @"email": kEmail + }]; + return responseUser; + }; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + id userInfoResponse = mockUserInfoWithDisplayName(kGoogleDisplayName, NO); + + [self signInAnonymouslyWithMockGetAccountInfoResponse:userInfoResponse + completion:^(FIRUser *user) { + // Pretend that the display name and providers on the server have been updated. + // Get account info is expected to be invoked twice. + id updatedMockUser = mockUserInfoWithDisplayName(kNewDisplayName, YES); + [self expectGetAccountInfoWithMockUserInfoResponse:updatedMockUser]; + [self expectGetAccountInfoWithMockUserInfoResponse:updatedMockUser]; + OCMExpect([_mockBackend setAccountInfo:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSetAccountInfoRequest *_Nullable request, + FIRSetAccountInfoResponseCallback callback) { + id mockSetAccountInfoResponse = OCMClassMock([FIRSetAccountInfoResponse class]); + OCMStub([mockSetAccountInfoResponse email]).andReturn(kNewEmail); + OCMStub([mockSetAccountInfoResponse displayName]).andReturn(kNewDisplayName); + callback(mockSetAccountInfoResponse, nil); + }); + XCTAssertTrue(user.isAnonymous); + + [user linkAndRetrieveDataWithCredential:linkEmailCredential + completion:^(FIRAuthDataResult *_Nullable linkAuthResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(error); + XCTAssertEqualObjects(user.email, kEmail); + XCTAssertFalse(user.isAnonymous); + [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. @@ -2222,6 +2333,34 @@ static const NSTimeInterval kExpectationTimeout = 2; OCMVerifyAll(_mockBackend); } +/** @fn signInAnonymouslyWithMockGetAccountInfoResponse:completion: + @brief Signs in with an anonymous 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)signInAnonymouslyWithMockGetAccountInfoResponse:(id)mockUserInfoResponse + completion:(void (^)(FIRUser *user))completion { + OCMExpect([_mockBackend signUpNewUser:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRSignUpNewUserRequest *_Nullable request, + FIRSignupNewUserCallback callback) { + id mockSignUpResponse = OCMClassMock([FIRSignUpNewUserResponse class]); + OCMStub([mockSignUpResponse IDToken]).andReturn(kAccessToken); + OCMStub([mockSignUpResponse approximateExpirationDate]) + .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]); + OCMStub([mockSignUpResponse refreshToken]).andReturn(kRefreshToken); + callback(mockSignUpResponse, nil); + }); + [self expectGetAccountInfoWithMockUserInfoResponse:mockUserInfoResponse]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] signInAnonymouslyWithCompletion:^(FIRAuthDataResult *_Nullable result, + NSError *_Nullable error) { + XCTAssertNotNil(result.user); + XCTAssertNil(error); + completion(result.user); + }]; +} + /** @fn signInWithEmailPasswordWithMockGetAccountInfoResponse:completion: @brief Signs in with an email and password account with mocked backend end calls. @param mockUserInfoResponse A mocked FIRGetAccountInfoResponseUser object. @@ -2229,7 +2368,7 @@ static const NSTimeInterval kExpectationTimeout = 2; parameter. */ - (void)signInWithEmailPasswordWithMockUserInfoResponse:(id)mockUserInfoResponse - completion:(void (^)(FIRUser *user))completion { + completion:(void (^)(FIRUser *user))completion { OCMExpect([_mockBackend verifyPassword:[OCMArg any] callback:[OCMArg any]]) .andCallBlock2(^(FIRVerifyPasswordRequest *_Nullable request, FIRVerifyPasswordResponseCallback callback) { @@ -2239,7 +2378,7 @@ static const NSTimeInterval kExpectationTimeout = 2; OCMStub([mockVeriyPasswordResponse approximateExpirationDate]) .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]); OCMStub([mockVeriyPasswordResponse refreshToken]).andReturn(kRefreshToken); - callback(mockVeriyPasswordResponse, nil); + callback(mockVeriyPasswordResponse, nil); }); }); [self expectGetAccountInfoWithMockUserInfoResponse:mockUserInfoResponse]; @@ -2253,6 +2392,35 @@ static const NSTimeInterval kExpectationTimeout = 2; }]; } +/** @fn signInWithEmailLinkWithMockGetAccountInfoResponse:completion: + @brief Signs in with an email link auth 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)signInWithEmailLinkWithMockUserInfoResponse:(id)mockUserInfoResponse + completion:(void (^)(FIRUser *user))completion { + OCMExpect([_mockBackend emailLinkSignin:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIREmailLinkSignInRequest *_Nullable request, + FIREmailLinkSigninResponseCallback callback) { + id mockVerifyLinkResponse = OCMClassMock([FIREmailLinkSignInResponse class]); + OCMStub([mockVerifyLinkResponse IDToken]).andReturn(kAccessToken); + OCMStub([mockVerifyLinkResponse approximateExpirationDate]) + .andReturn([NSDate dateWithTimeIntervalSinceNow:kAccessTokenTimeToLive]); + OCMStub([mockVerifyLinkResponse refreshToken]).andReturn(kRefreshToken); + callback(mockVerifyLinkResponse, nil); + }); + [self expectGetAccountInfoWithMockUserInfoResponse:mockUserInfoResponse]; + [[FIRAuth auth] signOut:NULL]; + [[FIRAuth auth] signInWithEmail:kEmail + link:@"https://www.google.com?oobCode=aCode&mode=signIn" + completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { + XCTAssertNotNil(result.user); + XCTAssertNil(error); + completion(result.user); + }]; +} + /** @fn expectGetAccountInfoWithMockUserInfoResponse: @brief Expects a GetAccountInfo request on the mock backend and calls back with provided fake account data. diff --git a/Firebase/Auth/CHANGELOG.md b/Firebase/Auth/CHANGELOG.md index 292f8bd..2669fc0 100644 --- a/Firebase/Auth/CHANGELOG.md +++ b/Firebase/Auth/CHANGELOG.md @@ -1,3 +1,7 @@ +# v5.0.2 +- Fix an issue where anonymous accounts weren't correctly promoted to + non-anonymous when linked with passwordless email auth accounts. + # v5.0.1 - Restore 4.x level of support for extensions (#1357). diff --git a/Firebase/Auth/Source/FIRUser.m b/Firebase/Auth/Source/FIRUser.m index 04aa861..3f5bf35 100644 --- a/Firebase/Auth/Source/FIRUser.m +++ b/Firebase/Auth/Source/FIRUser.m @@ -541,7 +541,7 @@ static void callInMainThreadWithAuthDataResultAndError( - (void)updateEmail:(nullable NSString *)email password:(nullable NSString *)password callback:(nonnull FIRUserProfileChangeCallback)callback { - if (password && ![password length]){ + if (password && ![password length]) { callback([FIRAuthErrorUtils weakPasswordErrorWithServerResponseReason:kMissingPasswordReason]); return; } @@ -561,11 +561,9 @@ static void callInMainThreadWithAuthDataResultAndError( return; } if (email) { - self->_email = email; + self->_email = [email copy]; } - if (self->_email && password) { - self->_anonymous = NO; - self->_hasEmailPasswordCredential = YES; + if (self->_email) { if (!hadEmailPasswordCredential) { // The list of providers need to be updated for the newly added email-password provider. [self internalGetTokenWithCallback:^(NSString *_Nullable accessToken, @@ -586,6 +584,20 @@ static void callInMainThreadWithAuthDataResultAndError( callback(error); return; } + for (FIRGetAccountInfoResponseUser *userAccountInfo in response.users) { + // Set the account to non-anonymous if there are any providers, even if + // they're not email/password ones. + if (userAccountInfo.providerUserInfo.count > 0) { + self->_anonymous = NO; + } + for (FIRGetAccountInfoResponseProviderUserInfo *providerUserInfo in + userAccountInfo.providerUserInfo) { + if ([providerUserInfo.providerID isEqualToString:FIREmailAuthProviderID]) { + self->_hasEmailPasswordCredential = YES; + break; + } + } + } [self updateWithGetAccountInfoResponse:response]; if (![self updateKeychain:&error]) { callback(error); -- cgit v1.2.3