Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to link/unlink a provider user id to an account. #359

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions FirebaseAdmin/FirebaseAdmin.IntegrationTests/Auth/UpdateUserTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2023, Google Inc. All rights reserved.
//
// 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.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FirebaseAdmin.Auth;
using Xunit;

namespace FirebaseAdmin.IntegrationTests.Auth
{
public class UpdateUserTest : IClassFixture<UpdateUserFixture>
{
private readonly UpdateUserFixture fixture;

public UpdateUserTest(UpdateUserFixture fixture)
{
this.fixture = fixture;
}

[Fact]
public async void CanUpdateProviderForUser()
{
var expectedProviderUid = $"google_{this.fixture.TestUser.Uid}";
const string expectedProviderId = "google.com";
const string expectedDisplayName = "Test";
const string expectedEmail = "[email protected]";
const string expectedPhone = "+11234567890";
const string expectedPhotoUrl = "https://www.example.com/image.png";

var userRecordArgs = new UserRecordArgs
{
Uid = this.fixture.TestUser.Uid,
ProviderToLink = new ProviderUserInfoArgs
{
Uid = expectedProviderUid,
ProviderId = expectedProviderId,
DisplayName = expectedDisplayName,
Email = expectedEmail,
PhoneNumber = expectedPhone,
PhotoUrl = expectedPhotoUrl,
},
};

await FirebaseAuth.DefaultInstance.UpdateUserAsync(userRecordArgs);

var userRecord = await FirebaseAuth.DefaultInstance.GetUserAsync(this.fixture.TestUser.Uid);

var provider = userRecord.ProviderData.SingleOrDefault(x => x.ProviderId == expectedProviderId);

Assert.NotNull(provider);

Assert.Equal(expectedProviderUid, provider.Uid);
Assert.Equal(expectedProviderId, provider.ProviderId);
Assert.Equal(expectedEmail, provider.Email);
// TODO: Apparently the accounts:update endpoint does not update the provider phone number even if
// it is specified in the UpdateUserRequest.ProviderToLink object
// Assert.Equal(expectedPhone, provider.PhoneNumber);
Assert.Equal(expectedPhotoUrl, provider.PhotoUrl);
}
}

public class UpdateUserFixture : IDisposable
{
private readonly TemporaryUserBuilder userBuilder;

public UpdateUserFixture()
{
IntegrationTestUtils.EnsureDefaultApp();
this.userBuilder = new TemporaryUserBuilder(FirebaseAuth.DefaultInstance);
this.TestUser = this.userBuilder.CreateRandomUserAsync().Result;
this.ImportUserUid = this.ImportUserAsync().Result;
}

public UserRecord TestUser { get; }

public string ImportUserUid { get; }

public void Dispose()
{
this.userBuilder.Dispose();
}

private async Task<string> ImportUserAsync()
{
var randomArgs = TemporaryUserBuilder.RandomUserRecordArgs();
var importUser = new ImportUserRecordArgs()
{
Uid = randomArgs.Uid,
Email = randomArgs.Email,
PhoneNumber = randomArgs.PhoneNumber,
};

var result = await FirebaseAuth.DefaultInstance.ImportUsersAsync(
new List<ImportUserRecordArgs>()
{
importUser,
});
Assert.Equal(1, result.SuccessCount);
this.userBuilder.AddUid(randomArgs.Uid);
return randomArgs.Uid;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,40 @@ namespace FirebaseAdmin.Auth.Users.Tests
{
public class FirebaseUserManagerTest
{
public static readonly IEnumerable<object[]> TestConfigs = new List<object[]>()
public static readonly TheoryData<TestConfig> TestConfigs = new TheoryData<TestConfig>()
{
new object[] { TestConfig.ForFirebaseAuth() },
new object[] { TestConfig.ForTenantAwareFirebaseAuth("tenant1") },
new object[] { TestConfig.ForFirebaseAuth().WithEmulator() },
new object[] { TestConfig.ForTenantAwareFirebaseAuth("tenant1").WithEmulator() },
TestConfig.ForFirebaseAuth(),
TestConfig.ForTenantAwareFirebaseAuth("tenant1"),
TestConfig.ForFirebaseAuth().WithEmulator(),
TestConfig.ForTenantAwareFirebaseAuth("tenant1").WithEmulator(),
};

public static readonly IEnumerable<object[]> MainTenantTestConfigs = new List<object[]>()
public static readonly TheoryData<TestConfig> MainTenantTestConfigs = new TheoryData<TestConfig>()
{
new object[] { TestConfig.ForFirebaseAuth() },
new object[] { TestConfig.ForFirebaseAuth().WithEmulator() },
TestConfig.ForFirebaseAuth(),
TestConfig.ForFirebaseAuth().WithEmulator(),
};

private const string CreateUserResponse = @"{""localId"": ""user1""}";

public static TheoryData<TestConfig, string, string> UpdateUserInvalidProviderToLinkTestData
{
get
{
var data = new TheoryData<TestConfig, string, string>();

foreach (var testConfigObj in TestConfigs)
{
var testConfig = (TestConfig)testConfigObj[0];

data.Add(testConfig, "google_user1", string.Empty); // Empty provider ID
data.Add(testConfig, string.Empty, "google.com"); // Empty provider UID
}

return data;
}
}

[Theory]
[MemberData(nameof(TestConfigs))]
public async Task GetUserById(TestConfig config)
Expand Down Expand Up @@ -1107,6 +1125,12 @@ public async Task UpdateUser(TestConfig config)
{ "package", "gold" },
};

var providerToLink = new ProviderUserInfoArgs()
{
Uid = "google_user1",
ProviderId = "google.com",
};

var user = await auth.UpdateUserAsync(new UserRecordArgs()
{
CustomClaims = customClaims,
Expand All @@ -1118,6 +1142,7 @@ public async Task UpdateUser(TestConfig config)
PhoneNumber = "+1234567890",
PhotoUrl = "https://example.com/user.png",
Uid = "user1",
ProviderToLink = providerToLink,
});

Assert.Equal("user1", user.Uid);
Expand All @@ -1135,6 +1160,13 @@ public async Task UpdateUser(TestConfig config)
Assert.Equal("+1234567890", request["phoneNumber"]);
Assert.Equal("https://example.com/user.png", request["photoUrl"]);

var expectedProviderUserInfo = new JObject
{
{ "Uid", "google_user1" },
{ "ProviderId", "google.com" },
};
Assert.Equal(expectedProviderUserInfo, request["linkProviderUserInfo"]);

var claims = NewtonsoftJsonSerializer.Instance.Deserialize<JObject>((string)request["customAttributes"]);
Assert.True((bool)claims["admin"]);
Assert.Equal(4L, claims["level"]);
Expand Down Expand Up @@ -1168,6 +1200,37 @@ public async Task UpdateUserPartial(TestConfig config)
Assert.True((bool)request["emailVerified"]);
}

[Theory]
[MemberData(nameof(TestConfigs))]
public async Task UpdateUserLinkProvider(TestConfig config)
{
var handler = new MockMessageHandler()
{
Response = new List<string>() { CreateUserResponse, config.GetUserResponse() },
};
var auth = config.CreateAuth(handler);

var user = await auth.UpdateUserAsync(new UserRecordArgs()
{
Uid = "user1",
ProviderToLink = new ProviderUserInfoArgs()
{
Uid = "google_user1",
ProviderId = "google.com",
},
});

Assert.Equal("user1", user.Uid);
Assert.Equal(2, handler.Requests.Count);
var request = NewtonsoftJsonSerializer.Instance.Deserialize<JObject>(handler.Requests[0].Body);
Assert.Equal(2, request.Count);
Assert.Equal("user1", request["localId"]);
var expectedProviderUserInfo = new JObject();
expectedProviderUserInfo.Add("Uid", "google_user1");
expectedProviderUserInfo.Add("ProviderId", "google.com");
Assert.Equal(expectedProviderUserInfo, request["linkProviderUserInfo"]);
}

[Theory]
[MemberData(nameof(TestConfigs))]
public async Task UpdateUserRemoveAttributes(TestConfig config)
Expand Down Expand Up @@ -1212,6 +1275,7 @@ public async Task UpdateUserRemoveProviders(TestConfig config)
{
PhoneNumber = null,
Uid = "user1",
ProvidersToDelete = new List<string>() { "google.com" },
});

Assert.Equal("user1", user.Uid);
Expand All @@ -1223,7 +1287,7 @@ public async Task UpdateUserRemoveProviders(TestConfig config)
Assert.Equal(2, request.Count);
Assert.Equal("user1", request["localId"]);
Assert.Equal(
new JArray() { "phone" },
new JArray() { "phone", "google.com" },
request["deleteProvider"]);
}

Expand Down Expand Up @@ -1485,6 +1549,110 @@ public async Task UpdateUserShortPassword(TestConfig config)
Assert.Empty(handler.Requests);
}

[Theory]
[MemberData(nameof(UpdateUserInvalidProviderToLinkTestData))]
public async Task UpdateUserInvalidProviderToLink(TestConfig config, string uid, string providerId)
{
var handler = new MockMessageHandler() { Response = CreateUserResponse };
var auth = config.CreateAuth(handler);

var args = new UserRecordArgs()
{
ProviderToLink = new ProviderUserInfoArgs()
{
Uid = uid,
ProviderId = providerId,
},
Uid = "user1",
};
await Assert.ThrowsAsync<ArgumentException>(
async () => await auth.UpdateUserAsync(args));
Assert.Empty(handler.Requests);
}

[Theory]
[MemberData(nameof(TestConfigs))]
public async Task UpdateUserInvalidEmailProviderToLink(TestConfig config)
{
var handler = new MockMessageHandler() { Response = CreateUserResponse };
var auth = config.CreateAuth(handler);

// Phone provider updated in 2 places in the same request
var args = new UserRecordArgs()
{
ProviderToLink = new ProviderUserInfoArgs()
{
Uid = "[email protected]",
ProviderId = "email",
},
Uid = "user1",
Email = "[email protected]",
};
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
async () => await auth.UpdateUserAsync(args));

const string expectedError = "Both UpdateRequest.Email and UpdateRequest.ProviderToLink.ProviderId='email' " +
"were set. To link to the email/password provider, only specify the " +
"UpdateRequest.Email field.";

Assert.Equal(expectedError, exception.Message);
Assert.Empty(handler.Requests);
}

[Theory]
[MemberData(nameof(TestConfigs))]
public async Task UpdateUserInvalidPhoneProviderToLink(TestConfig config)
{
var handler = new MockMessageHandler() { Response = CreateUserResponse };
var auth = config.CreateAuth(handler);

// Phone provider updated in 2 places in the same request
var args = new UserRecordArgs()
{
ProviderToLink = new ProviderUserInfoArgs()
{
Uid = "+11234567891",
ProviderId = "phone",
},
Uid = "user1",
PhoneNumber = "+11234567891",
};
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
async () => await auth.UpdateUserAsync(args));

const string expectedError = "Both UpdateRequest.PhoneNumber and UpdateRequest.ProviderToLink.ProviderId='phone'" +
" were set. To link to a phone provider, only specify the " +
"UpdateRequest.PhoneNumber field.";

Assert.Equal(expectedError, exception.Message);
Assert.Empty(handler.Requests);
}

[Theory]
[MemberData(nameof(TestConfigs))]
public async Task UpdateUserInvalidProvidersToDelete(TestConfig config)
{
var handler = new MockMessageHandler() { Response = CreateUserResponse };
var auth = config.CreateAuth(handler);

// Empty provider ID
var args = new UserRecordArgs()
{
ProvidersToDelete = new List<string>() { "google.com", string.Empty },
Uid = "user1",
};
await Assert.ThrowsAsync<ArgumentException>(
async () => await auth.UpdateUserAsync(args));
Assert.Empty(handler.Requests);

// Phone provider updates in two places
args.PhoneNumber = null;
args.ProvidersToDelete = new List<string>() { "google.com", "phone" };
await Assert.ThrowsAsync<ArgumentException>(
async () => await auth.UpdateUserAsync(args));
Assert.Empty(handler.Requests);
}

[Theory]
[MemberData(nameof(TestConfigs))]
public void EmptyNameClaims(TestConfig config)
Expand Down
2 changes: 1 addition & 1 deletion FirebaseAdmin/FirebaseAdmin/Auth/ProviderIdentifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public sealed class ProviderIdentifier : UserIdentifier
/// <param name="providerUid">The providerUid.</param>
public ProviderIdentifier(string providerId, string providerUid)
{
UserRecordArgs.CheckProvider(providerId, providerUid, required: true);
UserRecordArgs.CheckProvider(providerId, providerUid, true, true);
this.providerId = providerId;
this.providerUid = providerUid;
}
Expand Down
Loading