From 9a98371edd9eafb36549588137cd2ba6e5e548c2 Mon Sep 17 00:00:00 2001 From: Dev Date: Wed, 8 Apr 2026 16:01:46 +0200 Subject: [PATCH] feat(13-01): extend GraphDirectoryUser with UserType and add includeGuests parameter to directory service - Add string? UserType as last positional parameter to GraphDirectoryUser record - Add bool includeGuests = false parameter to IGraphUserDirectoryService.GetUsersAsync - Branch Graph filter: members-only (default) vs all users when includeGuests=true - Add userType to Graph Select array for MapUser population - Update MapUser to include UserType from Graph User object - Add MapUser_PopulatesUserType and MapUser_NullUserType tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../GraphUserDirectoryServiceTests.cs | 46 ++++++++++++++++++- .../Core/Models/GraphDirectoryUser.cs | 3 +- .../Services/GraphUserDirectoryService.cs | 11 +++-- .../Services/IGraphUserDirectoryService.cs | 5 ++ 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/SharepointToolbox.Tests/Services/GraphUserDirectoryServiceTests.cs b/SharepointToolbox.Tests/Services/GraphUserDirectoryServiceTests.cs index bfdf141..f0bef48 100644 --- a/SharepointToolbox.Tests/Services/GraphUserDirectoryServiceTests.cs +++ b/SharepointToolbox.Tests/Services/GraphUserDirectoryServiceTests.cs @@ -31,7 +31,8 @@ public class GraphUserDirectoryServiceTests UserPrincipalName = "alice@contoso.com", Mail = "alice@contoso.com", Department = "Engineering", - JobTitle = "Senior Developer" + JobTitle = "Senior Developer", + UserType = "Member" }; var result = GraphUserDirectoryService.MapUser(user); @@ -41,6 +42,7 @@ public class GraphUserDirectoryServiceTests Assert.Equal("alice@contoso.com", result.Mail); Assert.Equal("Engineering", result.Department); Assert.Equal("Senior Developer", result.JobTitle); + Assert.Equal("Member", result.UserType); } [Fact] @@ -52,7 +54,8 @@ public class GraphUserDirectoryServiceTests UserPrincipalName = "bob@contoso.com", Mail = null, Department = null, - JobTitle = null + JobTitle = null, + UserType = "Guest" }; var result = GraphUserDirectoryService.MapUser(user); @@ -62,6 +65,7 @@ public class GraphUserDirectoryServiceTests Assert.Null(result.Mail); Assert.Null(result.Department); Assert.Null(result.JobTitle); + Assert.Equal("Guest", result.UserType); } [Fact] @@ -120,6 +124,44 @@ public class GraphUserDirectoryServiceTests Assert.Null(result.JobTitle); } + // ── MapUser: UserType mapping ────────────────────────────────────────────── + + [Fact] + public void MapUser_PopulatesUserType() + { + var user = new User + { + DisplayName = "Eve Wilson", + UserPrincipalName = "eve@contoso.com", + Mail = "eve@contoso.com", + Department = "Sales", + JobTitle = "Account Executive", + UserType = "Member" + }; + + var result = GraphUserDirectoryService.MapUser(user); + + Assert.Equal("Member", result.UserType); + } + + [Fact] + public void MapUser_NullUserType_ReturnsNull() + { + var user = new User + { + DisplayName = "Frank Lee", + UserPrincipalName = "frank@contoso.com", + Mail = null, + Department = null, + JobTitle = null, + UserType = null + }; + + var result = GraphUserDirectoryService.MapUser(user); + + Assert.Null(result.UserType); + } + // ── GetUsersAsync: integration-level scenarios (skipped without live tenant) ── [Fact(Skip = "Requires integration test with real Graph client — PageIterator.CreatePageIterator " + diff --git a/SharepointToolbox/Core/Models/GraphDirectoryUser.cs b/SharepointToolbox/Core/Models/GraphDirectoryUser.cs index 839840a..22c8890 100644 --- a/SharepointToolbox/Core/Models/GraphDirectoryUser.cs +++ b/SharepointToolbox/Core/Models/GraphDirectoryUser.cs @@ -9,4 +9,5 @@ public record GraphDirectoryUser( string UserPrincipalName, string? Mail, string? Department, - string? JobTitle); + string? JobTitle, + string? UserType); diff --git a/SharepointToolbox/Services/GraphUserDirectoryService.cs b/SharepointToolbox/Services/GraphUserDirectoryService.cs index 63772b7..b2632bf 100644 --- a/SharepointToolbox/Services/GraphUserDirectoryService.cs +++ b/SharepointToolbox/Services/GraphUserDirectoryService.cs @@ -22,6 +22,7 @@ public class GraphUserDirectoryService : IGraphUserDirectoryService /// public async Task> GetUsersAsync( string clientId, + bool includeGuests = false, IProgress? progress = null, CancellationToken ct = default) { @@ -29,11 +30,12 @@ public class GraphUserDirectoryService : IGraphUserDirectoryService 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.Filter = includeGuests + ? "accountEnabled eq true" + : "accountEnabled eq true and userType eq 'Member'"; config.QueryParameters.Select = new[] { - "displayName", "userPrincipalName", "mail", "department", "jobTitle" + "displayName", "userPrincipalName", "mail", "department", "jobTitle", "userType" }; config.QueryParameters.Top = 999; // No ConsistencyLevel header: standard equality filter does not require eventual consistency @@ -74,5 +76,6 @@ public class GraphUserDirectoryService : IGraphUserDirectoryService UserPrincipalName: user.UserPrincipalName ?? string.Empty, Mail: user.Mail, Department: user.Department, - JobTitle: user.JobTitle); + JobTitle: user.JobTitle, + UserType: user.UserType); } diff --git a/SharepointToolbox/Services/IGraphUserDirectoryService.cs b/SharepointToolbox/Services/IGraphUserDirectoryService.cs index a3ba665..b60400b 100644 --- a/SharepointToolbox/Services/IGraphUserDirectoryService.cs +++ b/SharepointToolbox/Services/IGraphUserDirectoryService.cs @@ -13,6 +13,10 @@ public interface IGraphUserDirectoryService /// Iterates through all pages using the Graph SDK PageIterator until exhausted or cancelled. /// /// The client/tenant identifier used to obtain a Graph token. + /// + /// When false (default), only member users are returned (userType eq 'Member'). + /// When true, both members and guests are returned (no userType filter). + /// /// /// Optional progress reporter — receives the running count of users fetched so far. /// Phase 13's ViewModel uses this to show "Loading... X users" feedback. @@ -21,6 +25,7 @@ public interface IGraphUserDirectoryService /// Cancellation token. Iteration stops when cancelled. Task> GetUsersAsync( string clientId, + bool includeGuests = false, IProgress? progress = null, CancellationToken ct = default); }