using Microsoft.Graph; using Microsoft.Graph.Models; using SharepointToolbox.Core.Models; using AppGraphClientFactory = SharepointToolbox.Infrastructure.Auth.GraphClientFactory; namespace SharepointToolbox.Services; /// /// 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. /// public class GraphUserDirectoryService : IGraphUserDirectoryService { private readonly AppGraphClientFactory _graphClientFactory; public GraphUserDirectoryService(AppGraphClientFactory graphClientFactory) { _graphClientFactory = graphClientFactory; } /// public async Task> GetUsersAsync( string clientId, IProgress? 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(); var results = new List(); var pageIterator = PageIterator.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; } /// /// Maps a Graph SDK object to a record. /// Extracted as an internal static method to allow direct unit-test coverage of mapping logic /// without requiring a live Graph endpoint. /// 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); }