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) <noreply@anthropic.com>
This commit is contained in:
Dev
2026-04-08 16:01:46 +02:00
parent 0baa3695fe
commit 9a98371edd
4 changed files with 58 additions and 7 deletions

View File

@@ -31,7 +31,8 @@ public class GraphUserDirectoryServiceTests
UserPrincipalName = "alice@contoso.com", UserPrincipalName = "alice@contoso.com",
Mail = "alice@contoso.com", Mail = "alice@contoso.com",
Department = "Engineering", Department = "Engineering",
JobTitle = "Senior Developer" JobTitle = "Senior Developer",
UserType = "Member"
}; };
var result = GraphUserDirectoryService.MapUser(user); var result = GraphUserDirectoryService.MapUser(user);
@@ -41,6 +42,7 @@ public class GraphUserDirectoryServiceTests
Assert.Equal("alice@contoso.com", result.Mail); Assert.Equal("alice@contoso.com", result.Mail);
Assert.Equal("Engineering", result.Department); Assert.Equal("Engineering", result.Department);
Assert.Equal("Senior Developer", result.JobTitle); Assert.Equal("Senior Developer", result.JobTitle);
Assert.Equal("Member", result.UserType);
} }
[Fact] [Fact]
@@ -52,7 +54,8 @@ public class GraphUserDirectoryServiceTests
UserPrincipalName = "bob@contoso.com", UserPrincipalName = "bob@contoso.com",
Mail = null, Mail = null,
Department = null, Department = null,
JobTitle = null JobTitle = null,
UserType = "Guest"
}; };
var result = GraphUserDirectoryService.MapUser(user); var result = GraphUserDirectoryService.MapUser(user);
@@ -62,6 +65,7 @@ public class GraphUserDirectoryServiceTests
Assert.Null(result.Mail); Assert.Null(result.Mail);
Assert.Null(result.Department); Assert.Null(result.Department);
Assert.Null(result.JobTitle); Assert.Null(result.JobTitle);
Assert.Equal("Guest", result.UserType);
} }
[Fact] [Fact]
@@ -120,6 +124,44 @@ public class GraphUserDirectoryServiceTests
Assert.Null(result.JobTitle); 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) ── // ── GetUsersAsync: integration-level scenarios (skipped without live tenant) ──
[Fact(Skip = "Requires integration test with real Graph client — PageIterator.CreatePageIterator " + [Fact(Skip = "Requires integration test with real Graph client — PageIterator.CreatePageIterator " +

View File

@@ -9,4 +9,5 @@ public record GraphDirectoryUser(
string UserPrincipalName, string UserPrincipalName,
string? Mail, string? Mail,
string? Department, string? Department,
string? JobTitle); string? JobTitle,
string? UserType);

View File

@@ -22,6 +22,7 @@ public class GraphUserDirectoryService : IGraphUserDirectoryService
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<GraphDirectoryUser>> GetUsersAsync( public async Task<IReadOnlyList<GraphDirectoryUser>> GetUsersAsync(
string clientId, string clientId,
bool includeGuests = false,
IProgress<int>? progress = null, IProgress<int>? progress = null,
CancellationToken ct = default) CancellationToken ct = default)
{ {
@@ -29,11 +30,12 @@ public class GraphUserDirectoryService : IGraphUserDirectoryService
var response = await graphClient.Users.GetAsync(config => var response = await graphClient.Users.GetAsync(config =>
{ {
// Pending real-tenant verification — see STATE.md pending todos config.QueryParameters.Filter = includeGuests
config.QueryParameters.Filter = "accountEnabled eq true and userType eq 'Member'"; ? "accountEnabled eq true"
: "accountEnabled eq true and userType eq 'Member'";
config.QueryParameters.Select = new[] config.QueryParameters.Select = new[]
{ {
"displayName", "userPrincipalName", "mail", "department", "jobTitle" "displayName", "userPrincipalName", "mail", "department", "jobTitle", "userType"
}; };
config.QueryParameters.Top = 999; config.QueryParameters.Top = 999;
// No ConsistencyLevel header: standard equality filter does not require eventual consistency // No ConsistencyLevel header: standard equality filter does not require eventual consistency
@@ -74,5 +76,6 @@ public class GraphUserDirectoryService : IGraphUserDirectoryService
UserPrincipalName: user.UserPrincipalName ?? string.Empty, UserPrincipalName: user.UserPrincipalName ?? string.Empty,
Mail: user.Mail, Mail: user.Mail,
Department: user.Department, Department: user.Department,
JobTitle: user.JobTitle); JobTitle: user.JobTitle,
UserType: user.UserType);
} }

View File

@@ -13,6 +13,10 @@ public interface IGraphUserDirectoryService
/// Iterates through all pages using the Graph SDK PageIterator until exhausted or cancelled. /// Iterates through all pages using the Graph SDK PageIterator until exhausted or cancelled.
/// </summary> /// </summary>
/// <param name="clientId">The client/tenant identifier used to obtain a Graph token.</param> /// <param name="clientId">The client/tenant identifier used to obtain a Graph token.</param>
/// <param name="includeGuests">
/// When <c>false</c> (default), only member users are returned (userType eq 'Member').
/// When <c>true</c>, both members and guests are returned (no userType filter).
/// </param>
/// <param name="progress"> /// <param name="progress">
/// Optional progress reporter — receives the running count of users fetched so far. /// Optional progress reporter — receives the running count of users fetched so far.
/// Phase 13's ViewModel uses this to show "Loading... X users" feedback. /// Phase 13's ViewModel uses this to show "Loading... X users" feedback.
@@ -21,6 +25,7 @@ public interface IGraphUserDirectoryService
/// <param name="ct">Cancellation token. Iteration stops when cancelled.</param> /// <param name="ct">Cancellation token. Iteration stops when cancelled.</param>
Task<IReadOnlyList<GraphDirectoryUser>> GetUsersAsync( Task<IReadOnlyList<GraphDirectoryUser>> GetUsersAsync(
string clientId, string clientId,
bool includeGuests = false,
IProgress<int>? progress = null, IProgress<int>? progress = null,
CancellationToken ct = default); CancellationToken ct = default);
} }