Skip to content

Commit 3b831b4

Browse files
authored
fix(auth): Fixing the error response parsing of ImportUsersAsync() (#224)
1 parent 6bef3b2 commit 3b831b4

File tree

10 files changed

+231
-103
lines changed

10 files changed

+231
-103
lines changed

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -162,25 +162,6 @@ await Assert.ThrowsAsync<ArgumentException>(
162162
async () => await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync("user1", customClaims));
163163
}
164164

165-
[Fact]
166-
public async Task ImportUsersPasswordNoHash()
167-
{
168-
var args = new ImportUserRecordArgs()
169-
{
170-
Uid = "123",
171-
Email = "[email protected]",
172-
PasswordSalt = Encoding.ASCII.GetBytes("abc"),
173-
PasswordHash = Encoding.ASCII.GetBytes("def"),
174-
};
175-
176-
var usersLst = new List<ImportUserRecordArgs>()
177-
{
178-
args,
179-
};
180-
await Assert.ThrowsAsync<NullReferenceException>(
181-
async () => await FirebaseAuth.DefaultInstance.ImportUsersAsync(usersLst));
182-
}
183-
184165
public void Dispose()
185166
{
186167
FirebaseApp.DeleteAll();

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
using System.Net;
1919
using System.Net.Http;
2020
using System.Net.Http.Headers;
21+
using System.Text;
2122
using System.Threading.Tasks;
23+
using FirebaseAdmin.Auth.Hash;
2224
using FirebaseAdmin.Tests;
2325
using FirebaseAdmin.Util;
2426
using Google.Apis.Auth.OAuth2;
@@ -1871,6 +1873,183 @@ public async Task CreateSessionCookie()
18711873
this.AssertClientVersion(handler.LastRequestHeaders);
18721874
}
18731875

1876+
[Fact]
1877+
public async Task ImportUsers()
1878+
{
1879+
var handler = new MockMessageHandler()
1880+
{
1881+
Response = "{}",
1882+
};
1883+
var auth = this.CreateFirebaseAuth(handler);
1884+
var users = new List<ImportUserRecordArgs>()
1885+
{
1886+
new ImportUserRecordArgs() { Uid = "user1" },
1887+
new ImportUserRecordArgs() { Uid = "user2" },
1888+
};
1889+
1890+
var result = await auth.ImportUsersAsync(users);
1891+
1892+
Assert.Equal(2, result.SuccessCount);
1893+
Assert.Equal(0, result.FailureCount);
1894+
Assert.Empty(result.Errors);
1895+
1896+
Assert.Equal(1, handler.Requests.Count);
1897+
var expected = new JObject()
1898+
{
1899+
{
1900+
"users", new JArray()
1901+
{
1902+
new JObject() { { "localId", "user1" } },
1903+
new JObject() { { "localId", "user2" } },
1904+
}
1905+
},
1906+
};
1907+
var request = NewtonsoftJsonSerializer.Instance.Deserialize<JObject>(
1908+
handler.LastRequestBody);
1909+
Assert.True(JToken.DeepEquals(expected, request));
1910+
this.AssertClientVersion(handler.LastRequestHeaders);
1911+
}
1912+
1913+
[Fact]
1914+
public async Task ImportUsersError()
1915+
{
1916+
var handler = new MockMessageHandler()
1917+
{
1918+
Response = @"{
1919+
""error"": [
1920+
{""index"": 1, ""message"": ""test error""}
1921+
]
1922+
}",
1923+
};
1924+
var auth = this.CreateFirebaseAuth(handler);
1925+
var usersList = new List<ImportUserRecordArgs>()
1926+
{
1927+
new ImportUserRecordArgs() { Uid = "user1" },
1928+
new ImportUserRecordArgs() { Uid = "user2" },
1929+
};
1930+
1931+
var result = await auth.ImportUsersAsync(usersList);
1932+
1933+
Assert.Equal(1, result.SuccessCount);
1934+
Assert.Equal(1, result.FailureCount);
1935+
var error = Assert.Single(result.Errors);
1936+
Assert.Equal(1, error.Index);
1937+
Assert.Equal("test error", error.Reason);
1938+
1939+
Assert.Equal(1, handler.Requests.Count);
1940+
this.AssertClientVersion(handler.LastRequestHeaders);
1941+
}
1942+
1943+
[Fact]
1944+
public async Task ImportUsersWithPassword()
1945+
{
1946+
var handler = new MockMessageHandler()
1947+
{
1948+
Response = "{}",
1949+
};
1950+
var auth = this.CreateFirebaseAuth(handler);
1951+
var password = Encoding.UTF8.GetBytes("password");
1952+
var usersList = new List<ImportUserRecordArgs>()
1953+
{
1954+
new ImportUserRecordArgs() { Uid = "user1" },
1955+
new ImportUserRecordArgs()
1956+
{
1957+
Uid = "user2",
1958+
PasswordHash = password,
1959+
},
1960+
};
1961+
1962+
var result = await auth.ImportUsersAsync(usersList, new UserImportOptions()
1963+
{
1964+
Hash = new Bcrypt(),
1965+
});
1966+
1967+
Assert.Equal(2, result.SuccessCount);
1968+
Assert.Equal(0, result.FailureCount);
1969+
Assert.Empty(result.Errors);
1970+
1971+
Assert.Equal(1, handler.Requests.Count);
1972+
var expected = new JObject()
1973+
{
1974+
{
1975+
"users", new JArray()
1976+
{
1977+
new JObject() { { "localId", "user1" } },
1978+
new JObject()
1979+
{
1980+
{ "localId", "user2" },
1981+
{ "passwordHash", JwtUtils.UrlSafeBase64Encode(password) },
1982+
},
1983+
}
1984+
},
1985+
{ "hashAlgorithm", "BCRYPT" },
1986+
};
1987+
var request = NewtonsoftJsonSerializer.Instance.Deserialize<JObject>(
1988+
handler.LastRequestBody);
1989+
Assert.True(JToken.DeepEquals(expected, request));
1990+
this.AssertClientVersion(handler.LastRequestHeaders);
1991+
}
1992+
1993+
[Fact]
1994+
public async Task ImportUsersMissingHash()
1995+
{
1996+
var auth = this.CreateFirebaseAuth(new MockMessageHandler());
1997+
var usersList = new List<ImportUserRecordArgs>()
1998+
{
1999+
new ImportUserRecordArgs() { Uid = "user1" },
2000+
new ImportUserRecordArgs()
2001+
{
2002+
Uid = "user2",
2003+
PasswordHash = Encoding.UTF8.GetBytes("password"),
2004+
},
2005+
};
2006+
2007+
var exception = await Assert.ThrowsAsync<ArgumentException>(
2008+
() => auth.ImportUsersAsync(usersList));
2009+
2010+
Assert.Equal(
2011+
"UserImportHash option is required when at least one user has a password.",
2012+
exception.Message);
2013+
}
2014+
2015+
[Fact]
2016+
public async Task ImportUsersEmpty()
2017+
{
2018+
var auth = this.CreateFirebaseAuth(new MockMessageHandler());
2019+
2020+
var exception = await Assert.ThrowsAsync<ArgumentException>(
2021+
() => auth.ImportUsersAsync(new List<ImportUserRecordArgs>()));
2022+
2023+
Assert.Equal("Users must not be null or empty.", exception.Message);
2024+
}
2025+
2026+
[Fact]
2027+
public async Task ImportUsersNull()
2028+
{
2029+
var auth = this.CreateFirebaseAuth(new MockMessageHandler());
2030+
2031+
var exception = await Assert.ThrowsAsync<ArgumentException>(
2032+
() => auth.ImportUsersAsync(null));
2033+
2034+
Assert.Equal("Users must not be null or empty.", exception.Message);
2035+
}
2036+
2037+
[Fact]
2038+
public async Task ImportUsersExceedLimit()
2039+
{
2040+
var auth = this.CreateFirebaseAuth(new MockMessageHandler());
2041+
var users = new List<ImportUserRecordArgs>();
2042+
for (int i = 0; i < 1001; i++)
2043+
{
2044+
users.Add(new ImportUserRecordArgs() { Uid = $"user{i}" });
2045+
}
2046+
2047+
var exception = await Assert.ThrowsAsync<ArgumentException>(
2048+
() => auth.ImportUsersAsync(users));
2049+
2050+
Assert.Equal("Users list must not contain more than 1000 items.", exception.Message);
2051+
}
2052+
18742053
[Fact]
18752054
public async Task ServiceUnvailable()
18762055
{

FirebaseAdmin/FirebaseAdmin/Auth/ErrorInfo.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@
1313
// limitations under the License.
1414

1515
using System.Collections.Generic;
16+
using Newtonsoft.Json;
1617

1718
namespace FirebaseAdmin.Auth
1819
{
19-
// TODO(rsgowman): This class is expected to also be used for the
20-
// ImportUsersAsync() method... once that exists.
21-
2220
/// <summary>
23-
/// Represents an error encountered while deleting users via the
24-
/// <see cref="FirebaseAuth.DeleteUsersAsync(IReadOnlyList{string})"/> API.
21+
/// Represents an error encountered while performing a batch operation such as
22+
/// <see cref="FirebaseAuth.ImportUsersAsync(IEnumerable{ImportUserRecordArgs})"/> or
23+
/// <see cref="FirebaseAuth.DeleteUsersAsync(IReadOnlyList{string})"/>.
2524
/// </summary>
2625
public sealed class ErrorInfo
2726
{
@@ -31,15 +30,18 @@ internal ErrorInfo(int index, string reason)
3130
this.Reason = reason;
3231
}
3332

33+
internal ErrorInfo() { }
34+
3435
/// <summary>
35-
/// Gets the index of the user that was unable to be deleted in the list passed to the
36-
/// <see cref="FirebaseAuth.DeleteUsersAsync(IReadOnlyList{string})"/> method.
36+
/// Gets the index of the entry that caused the error.
3737
/// </summary>
38-
public int Index { get; }
38+
[JsonProperty("index")]
39+
public int Index { get; internal set; }
3940

4041
/// <summary>
4142
/// Gets a string describing the error.
4243
/// </summary>
43-
public string Reason { get; }
44+
[JsonProperty("message")]
45+
public string Reason { get; internal set; }
4446
}
4547
}

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,7 @@ public async Task<UserImportResult> ImportUsersAsync(
812812
{
813813
var request = new UserImportRequest(users, options);
814814
var userManager = this.IfNotDeleted(() => this.userManager.Value);
815-
return await userManager.ImportUsersAsync(request, cancellationToken);
815+
return await userManager.ImportUsersAsync(users, options, cancellationToken);
816816
}
817817

818818
/// <summary>

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -230,23 +230,14 @@ internal async Task<string> CreateUserAsync(
230230
}
231231

232232
internal async Task<UserImportResult> ImportUsersAsync(
233-
UserImportRequest request,
234-
CancellationToken cancellationToken)
233+
IEnumerable<ImportUserRecordArgs> users,
234+
UserImportOptions options,
235+
CancellationToken cancellationToken)
235236
{
236-
if (request == null)
237-
{
238-
throw new ArgumentNullException("The UserImportRequest request should not be null");
239-
}
240-
237+
var request = new UserImportRequest(users, options);
241238
var response = await this.PostAndDeserializeAsync<UploadAccountResponse>(
242239
"accounts:batchCreate", request, cancellationToken).ConfigureAwait(false);
243-
var uploadAccountResponse = response.Result;
244-
if (uploadAccountResponse == null)
245-
{
246-
throw new FirebaseAuthException(ErrorCode.Internal, "Failed to import users.");
247-
}
248-
249-
return new UserImportResult(request.GetUsersCount(), uploadAccountResponse.Errors);
240+
return new UserImportResult(request.UserCount, response.Result.Errors);
250241
}
251242

252243
/// <summary>
@@ -439,11 +430,8 @@ internal sealed class Args
439430
/// </summary>
440431
internal sealed class UploadAccountResponse
441432
{
442-
/// <summary>
443-
/// Gets the list of errors populated after a user import request by the identity toolkit.
444-
/// </summary>
445433
[JsonProperty("error")]
446-
public IReadOnlyList<ErrorInfo> Errors { get; }
434+
internal List<ErrorInfo> Errors { get; set; }
447435
}
448436

449437
/// <summary>

FirebaseAdmin/FirebaseAdmin/Auth/ImportUserRecordArgs.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ namespace FirebaseAdmin.Auth
2121
{
2222
/// <summary>
2323
/// Represents a user account to be imported to Firebase Auth via the
24-
/// <a cref="o:FirebaseAuth.ImportUsersAsync">FirebaseAuth.ImportUsersAsync</a> API. Must contain at least a
25-
/// user ID string.
24+
/// <see cref="FirebaseAuth.ImportUsersAsync(IEnumerable{ImportUserRecordArgs})"/> API. Must
25+
/// contain at least a user ID string.
2626
/// </summary>
2727
public sealed class ImportUserRecordArgs
2828
{

FirebaseAdmin/FirebaseAdmin/Auth/UserImportHash.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,8 @@ protected UserImportHash(string hashName)
5050
/// Retrieves the properties of the chosen hashing algorithm.
5151
/// </summary>
5252
/// <returns>Dictionary containing the specified properties of the hashing algorithm.</returns>
53-
internal IReadOnlyDictionary<string, object> GetProperties()
53+
internal Dictionary<string, object> GetProperties()
5454
{
55-
if (string.IsNullOrEmpty(this.hashName))
56-
{
57-
throw new ArgumentException("User import hash name must not be null or empty.");
58-
}
59-
6055
var options = this.GetHashConfiguration();
6156
var properties = new Dictionary<string, object>();
6257
foreach (var entry in options)

FirebaseAdmin/FirebaseAdmin/Auth/UserImportOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public sealed class UserImportOptions
3333
/// Retrieves properties of the password hashing algorithm.
3434
/// </summary>
3535
/// <returns>Dictionary containing key/values for password hashing properties.</returns>
36-
internal IReadOnlyDictionary<string, object> GetHashProperties()
36+
internal Dictionary<string, object> GetHashProperties()
3737
{
3838
if (this.Hash == null)
3939
{

0 commit comments

Comments
 (0)