- GraphUserDirectoryService uses PageIterator<User, UserCollectionResponse> for pagination - Filter: accountEnabled eq true and userType eq 'Member' (no ConsistencyLevel header) - Cancellation checked in PageIterator callback (return false stops iteration) - Progress reported via IProgress<int> with running count per user - MapUser extracted as internal static for direct unit test coverage - Tests: 5 unit tests for MapUser field mapping and fallback logic - Integration-level tests (pagination/cancellation) skipped with rationale documented - Note: test project compilation blocked by pre-existing BrandingServiceTests.cs (10-01 artifact)
79 lines
2.8 KiB
C#
79 lines
2.8 KiB
C#
using Microsoft.Graph;
|
|
using Microsoft.Graph.Models;
|
|
using SharepointToolbox.Core.Models;
|
|
using AppGraphClientFactory = SharepointToolbox.Infrastructure.Auth.GraphClientFactory;
|
|
|
|
namespace SharepointToolbox.Services;
|
|
|
|
/// <summary>
|
|
/// Enumerates all enabled member users from a tenant via Microsoft Graph,
|
|
/// using PageIterator for transparent multi-page iteration.
|
|
/// Used by Phase 13's User Directory ViewModel.
|
|
/// </summary>
|
|
public class GraphUserDirectoryService : IGraphUserDirectoryService
|
|
{
|
|
private readonly AppGraphClientFactory _graphClientFactory;
|
|
|
|
public GraphUserDirectoryService(AppGraphClientFactory graphClientFactory)
|
|
{
|
|
_graphClientFactory = graphClientFactory;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IReadOnlyList<GraphDirectoryUser>> GetUsersAsync(
|
|
string clientId,
|
|
IProgress<int>? progress = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var graphClient = await _graphClientFactory.CreateClientAsync(clientId, ct);
|
|
|
|
var response = await graphClient.Users.GetAsync(config =>
|
|
{
|
|
// Pending real-tenant verification — see STATE.md pending todos
|
|
config.QueryParameters.Filter = "accountEnabled eq true and userType eq 'Member'";
|
|
config.QueryParameters.Select = new[]
|
|
{
|
|
"displayName", "userPrincipalName", "mail", "department", "jobTitle"
|
|
};
|
|
config.QueryParameters.Top = 999;
|
|
// No ConsistencyLevel header: standard equality filter does not require eventual consistency
|
|
}, ct);
|
|
|
|
if (response is null)
|
|
return Array.Empty<GraphDirectoryUser>();
|
|
|
|
var results = new List<GraphDirectoryUser>();
|
|
|
|
var pageIterator = PageIterator<User, UserCollectionResponse>.CreatePageIterator(
|
|
graphClient,
|
|
response,
|
|
user =>
|
|
{
|
|
// Honour cancellation inside the callback — returning false stops iteration
|
|
if (ct.IsCancellationRequested)
|
|
return false;
|
|
|
|
results.Add(MapUser(user));
|
|
progress?.Report(results.Count);
|
|
return true;
|
|
});
|
|
|
|
await pageIterator.IterateAsync(ct);
|
|
|
|
return results;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a Graph SDK <see cref="User"/> object to a <see cref="GraphDirectoryUser"/> record.
|
|
/// Extracted as an internal static method to allow direct unit-test coverage of mapping logic
|
|
/// without requiring a live Graph endpoint.
|
|
/// </summary>
|
|
internal static GraphDirectoryUser MapUser(User user) =>
|
|
new(
|
|
DisplayName: user.DisplayName ?? user.UserPrincipalName ?? string.Empty,
|
|
UserPrincipalName: user.UserPrincipalName ?? string.Empty,
|
|
Mail: user.Mail,
|
|
Department: user.Department,
|
|
JobTitle: user.JobTitle);
|
|
}
|