Skip to content

Commit a1ee19c

Browse files
authored
Merge pull request #58 from ChristopherLenz/list-users
Adding the ListUsers API
2 parents a7ee4bb + 8ff4b8f commit a1ee19c

16 files changed

+637
-12
lines changed

FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Google.Apis.Auth" Version="1.35.1" />
11+
<PackageReference Include="Google.Apis.Auth" Version="1.40.0" />
1212
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
1313
<PackageReference Include="xunit" Version="2.3.1" />
1414
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />

FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Google.Apis.Auth" Version="1.35.1" />
11+
<PackageReference Include="Google.Apis.Auth" Version="1.40.0" />
1212
</ItemGroup>
1313

1414
<ItemGroup>

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414

1515
using System;
1616
using System.Collections.Generic;
17+
using System.Linq;
1718
using System.Net;
1819
using System.Net.Http;
1920
using System.Threading.Tasks;
2021
using FirebaseAdmin.Tests;
22+
using Google.Api.Gax;
23+
using Google.Api.Gax.Rest;
2124
using Google.Apis.Auth.OAuth2;
2225
using Google.Apis.Json;
2326
using Newtonsoft.Json.Linq;
@@ -296,6 +299,146 @@ public async Task GetUserByPhoneNumberEmpty()
296299
await Assert.ThrowsAsync<ArgumentException>(() => userManager.GetUserByPhoneNumberAsync(string.Empty));
297300
}
298301

302+
[Fact]
303+
public async Task ListUsersPaged()
304+
{
305+
var nextPageToken = Guid.NewGuid().ToString();
306+
var firstCallHandler = new MockMessageHandler()
307+
{
308+
Response = new DownloadAccountResponse()
309+
{
310+
NextPageToken = nextPageToken,
311+
Users = new List<GetAccountInfoResponse.User>()
312+
{
313+
new GetAccountInfoResponse.User() { UserId = "user1" },
314+
new GetAccountInfoResponse.User() { UserId = "user2" },
315+
new GetAccountInfoResponse.User() { UserId = "user3" },
316+
},
317+
},
318+
};
319+
320+
var secondCallHandler = new MockMessageHandler()
321+
{
322+
Response = new DownloadAccountResponse()
323+
{
324+
NextPageToken = string.Empty,
325+
Users = new List<GetAccountInfoResponse.User>()
326+
{
327+
new GetAccountInfoResponse.User() { UserId = "user4" },
328+
new GetAccountInfoResponse.User() { UserId = "user5" },
329+
new GetAccountInfoResponse.User() { UserId = "user6" },
330+
},
331+
},
332+
};
333+
334+
var factory = new MockHttpClientFactory(new MultipleMockMessageHandler(new Dictionary<Func<HttpRequestMessage, bool>, MockMessageHandler>
335+
{
336+
{ initMessage => initMessage.RequestUri.Query.Equals("?maxResults=3&nextPageToken="), firstCallHandler },
337+
{ initMessage => initMessage.RequestUri.Query.Equals($"?maxResults=3&nextPageToken={nextPageToken}"), secondCallHandler },
338+
}));
339+
340+
var userManager = new FirebaseUserManager(
341+
new FirebaseUserManagerArgs
342+
{
343+
Credential = MockCredential,
344+
ProjectId = MockProjectId,
345+
ClientFactory = factory,
346+
});
347+
348+
var usersPage = userManager.ListUsers(new ListUsersOptions());
349+
350+
var users = new List<ExportedUserRecord>();
351+
var pageCounter = 0;
352+
353+
for (Page<ExportedUserRecord> userPage; (userPage = await usersPage.ReadPageAsync(3)) != null;)
354+
{
355+
pageCounter++;
356+
users.AddRange(userPage);
357+
358+
if (string.IsNullOrEmpty(userPage.NextPageToken))
359+
{
360+
break;
361+
}
362+
}
363+
364+
Assert.Equal(6, users.Count);
365+
Assert.Equal(2, pageCounter);
366+
Assert.Equal("user1", users[0].Uid);
367+
Assert.Equal("user2", users[1].Uid);
368+
Assert.Equal("user3", users[2].Uid);
369+
Assert.Equal("user4", users[3].Uid);
370+
Assert.Equal("user5", users[4].Uid);
371+
Assert.Equal("user6", users[5].Uid);
372+
}
373+
374+
[Fact]
375+
public async Task ListUsers()
376+
{
377+
var nextPageToken = Guid.NewGuid().ToString();
378+
var handler = new MockMessageHandler()
379+
{
380+
Response = new DownloadAccountResponse()
381+
{
382+
NextPageToken = nextPageToken,
383+
Users = new List<GetAccountInfoResponse.User>()
384+
{
385+
new GetAccountInfoResponse.User() { UserId = "user1" },
386+
new GetAccountInfoResponse.User() { UserId = "user2" },
387+
new GetAccountInfoResponse.User() { UserId = "user3" },
388+
},
389+
},
390+
};
391+
392+
var factory = new MockHttpClientFactory(handler);
393+
var userManager = new FirebaseUserManager(
394+
new FirebaseUserManagerArgs
395+
{
396+
Credential = MockCredential,
397+
ProjectId = MockProjectId,
398+
ClientFactory = factory,
399+
});
400+
401+
var usersPage = userManager.ListUsers(new ListUsersOptions());
402+
var listUsersRequest = await usersPage.ReadPageAsync(3);
403+
var userRecords = listUsersRequest.ToList();
404+
Assert.Equal(nextPageToken, listUsersRequest.NextPageToken);
405+
Assert.Equal(3, userRecords.Count);
406+
Assert.Equal("user1", userRecords[0].Uid);
407+
Assert.Equal("user2", userRecords[1].Uid);
408+
Assert.Equal("user3", userRecords[2].Uid);
409+
}
410+
411+
[Fact]
412+
public void ListUsersRequestOptionsAreSet()
413+
{
414+
var handler = new MockMessageHandler()
415+
{
416+
};
417+
418+
var factory = new MockHttpClientFactory(handler);
419+
var userManager = new FirebaseUserManager(
420+
new FirebaseUserManagerArgs
421+
{
422+
Credential = MockCredential,
423+
ProjectId = MockProjectId,
424+
ClientFactory = factory,
425+
});
426+
427+
var listUsersRequest = userManager.CreateListUserRequest(new ListUsersOptions());
428+
429+
// by default they are set
430+
Assert.True(listUsersRequest.RequestParameters.ContainsKey("maxResults"));
431+
Assert.True(listUsersRequest.RequestParameters.ContainsKey("nextPageToken"));
432+
Assert.Equal(FirebaseUserManager.MaxListUsersResults, int.Parse(listUsersRequest.RequestParameters["maxResults"].DefaultValue));
433+
Assert.Null(listUsersRequest.RequestParameters["nextPageToken"].DefaultValue);
434+
435+
// change the values and check again
436+
listUsersRequest.SetPageSize(10);
437+
listUsersRequest.SetPageToken("theNewNextPageToken");
438+
Assert.Equal(10, int.Parse(listUsersRequest.RequestParameters["maxResults"].DefaultValue));
439+
Assert.Equal("theNewNextPageToken", listUsersRequest.RequestParameters["nextPageToken"].DefaultValue);
440+
}
441+
299442
[Fact]
300443
public async Task CreateUser()
301444
{

FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13-
<PackageReference Include="Google.Apis.Auth" Version="1.35.1" />
13+
<PackageReference Include="Google.Apis.Auth" Version="1.40.0" />
1414
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.0" />
1515
<PackageReference Include="xunit" Version="2.3.1" />
1616
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />

FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.Collections.Generic;
1617
using System.Net;
1718
using System.Net.Http;
1819
using System.Net.Http.Headers;
@@ -53,7 +54,7 @@ public MockMessageHandler()
5354

5455
public SetContentHeaders ApplyContentHeaders { get; set; }
5556

56-
protected override async Task<HttpResponseMessage> SendAsyncCore(
57+
protected internal override async Task<HttpResponseMessage> SendAsyncCore(
5758
HttpRequestMessage request, CancellationToken cancellationToken)
5859
{
5960
if (request.Content != null)
@@ -116,15 +117,41 @@ public int Calls
116117
get { return this.calls; }
117118
}
118119

120+
protected internal abstract Task<HttpResponseMessage> SendAsyncCore(
121+
HttpRequestMessage request, CancellationToken cancellationToken);
122+
119123
protected sealed override Task<HttpResponseMessage> SendAsync(
120124
HttpRequestMessage request, CancellationToken cancellationToken)
121125
{
122126
Interlocked.Increment(ref this.calls);
123127
return this.SendAsyncCore(request, cancellationToken);
124128
}
129+
}
125130

126-
protected abstract Task<HttpResponseMessage> SendAsyncCore(
127-
HttpRequestMessage request, CancellationToken cancellationToken);
131+
internal class MultipleMockMessageHandler : CountableMessageHandler
132+
{
133+
private readonly IDictionary<Func<HttpRequestMessage, bool>, MockMessageHandler> messageHandlers;
134+
135+
public MultipleMockMessageHandler(IDictionary<Func<HttpRequestMessage, bool>, MockMessageHandler> messageHandlers)
136+
{
137+
this.messageHandlers = messageHandlers;
138+
}
139+
140+
protected internal override async Task<HttpResponseMessage> SendAsyncCore(
141+
HttpRequestMessage request, CancellationToken cancellationToken)
142+
{
143+
foreach (var (requestCheck, mockMessageHandler) in this.messageHandlers)
144+
{
145+
// check if the messagehandler is responsible for the current request
146+
if (requestCheck.Invoke(request))
147+
{
148+
this.messageHandlers.Remove(requestCheck);
149+
return await mockMessageHandler.SendAsyncCore(request, cancellationToken);
150+
}
151+
}
152+
153+
return new HttpResponseMessage(HttpStatusCode.NotFound);
154+
}
128155
}
129156

130157
internal class MockHttpClientFactory : HttpClientFactory
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2019, Google Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace FirebaseAdmin.Auth
16+
{
17+
/// <summary>
18+
/// Contains metadata associated with a Firebase user account, along with password hash and salt.
19+
/// Instances of this class are immutable and thread safe.
20+
/// </summary>
21+
public sealed class ExportedUserRecord : UserRecord
22+
{
23+
private readonly string passwordHash;
24+
private readonly string passwordSalt;
25+
26+
internal ExportedUserRecord(GetAccountInfoResponse.User user)
27+
: base(user)
28+
{
29+
this.passwordHash = user.PasswordHash;
30+
this.passwordSalt = user.PasswordSalt;
31+
}
32+
33+
/// <summary>
34+
/// Gets the user's password hash as a base64-encoded string.
35+
/// If the Firebase Auth hashing algorithm (SCRYPT) was used to create the user account,
36+
/// returns the base64-encoded password hash of the user.If a different hashing algorithm was
37+
/// used to create this user, as is typical when migrating from another Auth system, returns
38+
/// an empty string. Returns null if no password is set.
39+
/// </summary>
40+
public string PasswordHash
41+
{
42+
get => this.passwordHash;
43+
}
44+
45+
/// <summary>
46+
/// Gets the user's password salt as a base64-encoded string.
47+
/// If the Firebase Auth hashing algorithm (SCRYPT) was used to create the user account,
48+
/// returns the base64-encoded password salt of the user.If a different hashing algorithm was
49+
/// used to create this user, as is typical when migrating from another Auth system, returns
50+
/// an empty string. Returns null if no password is set.
51+
/// </summary>
52+
public string PasswordSalt
53+
{
54+
get => this.passwordSalt;
55+
}
56+
}
57+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2019, Google Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System.Collections.Generic;
16+
17+
namespace FirebaseAdmin.Auth
18+
{
19+
/// <summary>
20+
/// Contains a collection of Firebase user accounts.
21+
/// </summary>
22+
public sealed class ExportedUserRecords
23+
{
24+
/// <summary>
25+
/// Gets or sets the next page link.
26+
/// </summary>
27+
public string NextPageToken { get; set; }
28+
29+
/// <summary>
30+
/// Gets or sets the users.
31+
/// </summary>
32+
public List<ExportedUserRecord> Users { get; set; }
33+
}
34+
}

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
using System.Collections.Generic;
1717
using System.Threading;
1818
using System.Threading.Tasks;
19+
using Google.Api.Gax;
20+
using Google.Api.Gax.Rest;
1921

2022
namespace FirebaseAdmin.Auth
2123
{
@@ -523,6 +525,18 @@ public async Task SetCustomUserClaimsAsync(
523525
await userManager.UpdateUserAsync(user, cancellationToken).ConfigureAwait(false);
524526
}
525527

528+
/// <summary>
529+
/// Gets an async enumerable to page users starting from the specified pageToken. If the pageToken is empty, it starts from the first page.
530+
/// </summary>
531+
/// <param name="requestOptions">The options for the next remote call.</param>
532+
/// <returns>A <see cref="PagedAsyncEnumerable{DownloadAccountResponse, UserRecord}"/> instance.</returns>
533+
public PagedAsyncEnumerable<ExportedUserRecords, ExportedUserRecord> ListUsersAsync(ListUsersOptions requestOptions)
534+
{
535+
var userManager = this.IfNotDeleted(() => this.userManager.Value);
536+
537+
return userManager.ListUsers(requestOptions);
538+
}
539+
526540
/// <summary>
527541
/// Deletes this <see cref="FirebaseAuth"/> service instance.
528542
/// </summary>

0 commit comments

Comments
 (0)