530 lines
22 KiB
Markdown
530 lines
22 KiB
Markdown
---
|
|
phase: 13-user-directory-viewmodel
|
|
plan: 02
|
|
type: execute
|
|
wave: 2
|
|
depends_on: [13-01]
|
|
files_modified:
|
|
- SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
|
|
- SharepointToolbox/App.xaml.cs
|
|
- SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelDirectoryTests.cs
|
|
autonomous: true
|
|
requirements:
|
|
- UDIR-01
|
|
- UDIR-02
|
|
- UDIR-03
|
|
- UDIR-04
|
|
|
|
must_haves:
|
|
truths:
|
|
- "UserAccessAuditViewModel exposes an IsBrowseMode bool toggle property that switches between Search and Browse modes"
|
|
- "When IsBrowseMode is false (default), all existing people-picker behavior works identically (no regression)"
|
|
- "LoadDirectoryCommand calls IGraphUserDirectoryService.GetUsersAsync with includeGuests=true, reports progress via DirectoryLoadStatus, supports cancellation via CancelDirectoryLoadCommand"
|
|
- "DirectoryUsers (ObservableCollection<GraphDirectoryUser>) is populated after load completes"
|
|
- "DirectoryUsersView (ICollectionView) wraps DirectoryUsers with filtering by IncludeGuests toggle and DirectoryFilterText, and default SortDescription on DisplayName"
|
|
- "IncludeGuests toggle filters DirectoryUsersView in-memory by UserType without issuing a new Graph request"
|
|
- "DirectoryFilterText filters by DisplayName, UserPrincipalName, Department, and JobTitle"
|
|
- "Each user row in DirectoryUsersView exposes DisplayName, UserPrincipalName, Department, and JobTitle (via GraphDirectoryUser properties)"
|
|
- "IsLoadingDirectory is true while directory load is in progress, false otherwise"
|
|
- "CancelDirectoryLoadCommand cancels the in-flight directory load and sets IsLoadingDirectory to false"
|
|
- "OnTenantSwitched clears directory state (DirectoryUsers, DirectoryFilterText, IsBrowseMode)"
|
|
- "IGraphUserDirectoryService is injected via constructor and registered in DI"
|
|
artifacts:
|
|
- path: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
|
|
provides: "Directory browse mode with paginated load, progress, cancellation, filtering, sorting"
|
|
contains: "IsBrowseMode"
|
|
- path: "SharepointToolbox/App.xaml.cs"
|
|
provides: "DI wiring for IGraphUserDirectoryService into UserAccessAuditViewModel"
|
|
contains: "IGraphUserDirectoryService"
|
|
- path: "SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelDirectoryTests.cs"
|
|
provides: "Comprehensive tests for directory browse mode"
|
|
min_lines: 100
|
|
key_links:
|
|
- from: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
|
|
to: "SharepointToolbox/Services/IGraphUserDirectoryService.cs"
|
|
via: "constructor injection"
|
|
pattern: "IGraphUserDirectoryService"
|
|
- from: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
|
|
to: "SharepointToolbox/Core/Models/GraphDirectoryUser.cs"
|
|
via: "collection element type"
|
|
pattern: "ObservableCollection<GraphDirectoryUser>"
|
|
---
|
|
|
|
<objective>
|
|
Add directory browse mode to UserAccessAuditViewModel with paginated load, progress, cancellation, member/guest filtering, text search, and sorting — all fully testable without the View.
|
|
|
|
Purpose: Implements SC1-SC4 for Phase 13. Administrators get a toggle between the existing people-picker search and a new directory browse mode that loads all tenant users, supports member/guest filtering, and displays Department/JobTitle columns.
|
|
|
|
Output: Updated ViewModel with directory browse mode, DI registration, and comprehensive test coverage.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md
|
|
@C:/Users/dev/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/phases/13-user-directory-viewmodel/13-RESEARCH.md
|
|
|
|
<interfaces>
|
|
<!-- IGraphUserDirectoryService (after 13-01) -->
|
|
From SharepointToolbox/Services/IGraphUserDirectoryService.cs:
|
|
```csharp
|
|
public interface IGraphUserDirectoryService
|
|
{
|
|
Task<IReadOnlyList<GraphDirectoryUser>> GetUsersAsync(
|
|
string clientId,
|
|
bool includeGuests = false,
|
|
IProgress<int>? progress = null,
|
|
CancellationToken ct = default);
|
|
}
|
|
```
|
|
|
|
<!-- GraphDirectoryUser (after 13-01) -->
|
|
From SharepointToolbox/Core/Models/GraphDirectoryUser.cs:
|
|
```csharp
|
|
public record GraphDirectoryUser(
|
|
string DisplayName, string UserPrincipalName,
|
|
string? Mail, string? Department, string? JobTitle, string? UserType);
|
|
```
|
|
|
|
<!-- Current ViewModel constructors -->
|
|
From SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs:
|
|
```csharp
|
|
// Full constructor (DI):
|
|
public UserAccessAuditViewModel(
|
|
IUserAccessAuditService auditService,
|
|
IGraphUserSearchService graphUserSearchService,
|
|
ISessionManager sessionManager,
|
|
UserAccessCsvExportService csvExportService,
|
|
UserAccessHtmlExportService htmlExportService,
|
|
IBrandingService brandingService,
|
|
ILogger<FeatureViewModelBase> logger)
|
|
|
|
// Test constructor:
|
|
internal UserAccessAuditViewModel(
|
|
IUserAccessAuditService auditService,
|
|
IGraphUserSearchService graphUserSearchService,
|
|
ISessionManager sessionManager,
|
|
ILogger<FeatureViewModelBase> logger,
|
|
IBrandingService? brandingService = null)
|
|
```
|
|
|
|
<!-- FeatureViewModelBase patterns -->
|
|
From SharepointToolbox/ViewModels/FeatureViewModelBase.cs:
|
|
- IsRunning, StatusMessage, ProgressValue
|
|
- RunCommand / CancelCommand (uses own CTS)
|
|
- Protected abstract RunOperationAsync(ct, progress)
|
|
- OnTenantSwitched(profile) virtual override
|
|
|
|
<!-- Existing CollectionView pattern in same ViewModel -->
|
|
```csharp
|
|
var cvs = new CollectionViewSource { Source = Results };
|
|
ResultsView = cvs.View;
|
|
ResultsView.GroupDescriptions.Add(new PropertyGroupDescription(...));
|
|
ResultsView.Filter = FilterPredicate;
|
|
// On change: ResultsView.Refresh();
|
|
```
|
|
|
|
<!-- Existing test helper pattern -->
|
|
From SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs:
|
|
```csharp
|
|
private static (UserAccessAuditViewModel vm, Mock<IUserAccessAuditService> auditMock, Mock<IGraphUserSearchService> graphMock)
|
|
CreateViewModel(IReadOnlyList<UserAccessEntry>? auditResult = null)
|
|
{
|
|
var mockAudit = new Mock<IUserAccessAuditService>();
|
|
// ... setup
|
|
var vm = new UserAccessAuditViewModel(mockAudit.Object, mockGraph.Object, mockSession.Object,
|
|
NullLogger<FeatureViewModelBase>.Instance);
|
|
vm._currentProfile = new TenantProfile { ... };
|
|
return (vm, mockAudit, mockGraph);
|
|
}
|
|
```
|
|
|
|
<!-- DI registration pattern -->
|
|
From SharepointToolbox/App.xaml.cs:
|
|
```csharp
|
|
services.AddTransient<IGraphUserDirectoryService, GraphUserDirectoryService>();
|
|
// ...
|
|
services.AddTransient<UserAccessAuditViewModel>();
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add IGraphUserDirectoryService to ViewModel constructors and DI</name>
|
|
<files>
|
|
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs,
|
|
SharepointToolbox/App.xaml.cs
|
|
</files>
|
|
<behavior>
|
|
- Full constructor accepts IGraphUserDirectoryService as a parameter
|
|
- Test constructor accepts IGraphUserDirectoryService? as optional parameter
|
|
- Field _graphUserDirectoryService stores the injected service
|
|
- App.xaml.cs DI resolves IGraphUserDirectoryService for UserAccessAuditViewModel
|
|
</behavior>
|
|
<action>
|
|
1. Add field to ViewModel:
|
|
```csharp
|
|
private readonly IGraphUserDirectoryService? _graphUserDirectoryService;
|
|
```
|
|
|
|
2. Update full constructor — add `IGraphUserDirectoryService graphUserDirectoryService` parameter after `brandingService`:
|
|
```csharp
|
|
public UserAccessAuditViewModel(
|
|
IUserAccessAuditService auditService,
|
|
IGraphUserSearchService graphUserSearchService,
|
|
ISessionManager sessionManager,
|
|
UserAccessCsvExportService csvExportService,
|
|
UserAccessHtmlExportService htmlExportService,
|
|
IBrandingService brandingService,
|
|
IGraphUserDirectoryService graphUserDirectoryService,
|
|
ILogger<FeatureViewModelBase> logger)
|
|
```
|
|
Assign `_graphUserDirectoryService = graphUserDirectoryService;`
|
|
|
|
3. Update test constructor — add optional parameter:
|
|
```csharp
|
|
internal UserAccessAuditViewModel(
|
|
IUserAccessAuditService auditService,
|
|
IGraphUserSearchService graphUserSearchService,
|
|
ISessionManager sessionManager,
|
|
ILogger<FeatureViewModelBase> logger,
|
|
IBrandingService? brandingService = null,
|
|
IGraphUserDirectoryService? graphUserDirectoryService = null)
|
|
```
|
|
Assign `_graphUserDirectoryService = graphUserDirectoryService;`
|
|
|
|
4. In App.xaml.cs, the existing DI registration for `UserAccessAuditViewModel` is Transient and uses constructor injection — since `IGraphUserDirectoryService` is already registered as Transient, DI auto-resolves it. No change needed in App.xaml.cs unless the constructor parameter order requires explicit factory. Verify by building.
|
|
</action>
|
|
<verify>
|
|
<automated>dotnet build --no-restore -warnaserror</automated>
|
|
</verify>
|
|
<done>IGraphUserDirectoryService injected into ViewModel. DI resolves it automatically. Build passes.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add directory browse mode properties and commands</name>
|
|
<files>
|
|
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
|
|
</files>
|
|
<behavior>
|
|
- IsBrowseMode (bool) toggle property, default false
|
|
- DirectoryUsers (ObservableCollection of GraphDirectoryUser)
|
|
- DirectoryUsersView (ICollectionView) with filter and default sort on DisplayName
|
|
- IsLoadingDirectory (bool) loading indicator
|
|
- DirectoryLoadStatus (string) for "Loading... X users" display
|
|
- IncludeGuests (bool) toggle for member/guest filtering
|
|
- DirectoryFilterText (string) for text search
|
|
- DirectoryUserCount (int) computed property showing filtered count
|
|
- LoadDirectoryCommand (IAsyncRelayCommand)
|
|
- CancelDirectoryLoadCommand (RelayCommand)
|
|
- Own CancellationTokenSource for directory load (separate from base class CTS)
|
|
</behavior>
|
|
<action>
|
|
1. Add observable properties:
|
|
```csharp
|
|
[ObservableProperty]
|
|
private bool _isBrowseMode;
|
|
|
|
[ObservableProperty]
|
|
private ObservableCollection<GraphDirectoryUser> _directoryUsers = new();
|
|
|
|
[ObservableProperty]
|
|
private bool _isLoadingDirectory;
|
|
|
|
[ObservableProperty]
|
|
private string _directoryLoadStatus = string.Empty;
|
|
|
|
[ObservableProperty]
|
|
private bool _includeGuests;
|
|
|
|
[ObservableProperty]
|
|
private string _directoryFilterText = string.Empty;
|
|
```
|
|
|
|
2. Add computed property:
|
|
```csharp
|
|
public int DirectoryUserCount => DirectoryUsersView?.Cast<object>().Count() ?? 0;
|
|
```
|
|
|
|
3. Add ICollectionView + CTS:
|
|
```csharp
|
|
public ICollectionView DirectoryUsersView { get; }
|
|
private CancellationTokenSource? _directoryCts;
|
|
```
|
|
|
|
4. Add commands:
|
|
```csharp
|
|
public IAsyncRelayCommand LoadDirectoryCommand { get; }
|
|
public RelayCommand CancelDirectoryLoadCommand { get; }
|
|
```
|
|
|
|
5. Initialize in BOTH constructors (after existing init):
|
|
```csharp
|
|
var dirCvs = new CollectionViewSource { Source = DirectoryUsers };
|
|
DirectoryUsersView = dirCvs.View;
|
|
DirectoryUsersView.SortDescriptions.Add(
|
|
new SortDescription(nameof(GraphDirectoryUser.DisplayName), ListSortDirection.Ascending));
|
|
DirectoryUsersView.Filter = DirectoryFilterPredicate;
|
|
|
|
LoadDirectoryCommand = new AsyncRelayCommand(LoadDirectoryAsync, () => !IsLoadingDirectory);
|
|
CancelDirectoryLoadCommand = new RelayCommand(
|
|
() => _directoryCts?.Cancel(),
|
|
() => IsLoadingDirectory);
|
|
```
|
|
|
|
6. Add change handlers:
|
|
```csharp
|
|
partial void OnIncludeGuestsChanged(bool value)
|
|
{
|
|
DirectoryUsersView.Refresh();
|
|
OnPropertyChanged(nameof(DirectoryUserCount));
|
|
}
|
|
|
|
partial void OnDirectoryFilterTextChanged(string value)
|
|
{
|
|
DirectoryUsersView.Refresh();
|
|
OnPropertyChanged(nameof(DirectoryUserCount));
|
|
}
|
|
|
|
partial void OnIsLoadingDirectoryChanged(bool value)
|
|
{
|
|
LoadDirectoryCommand.NotifyCanExecuteChanged();
|
|
CancelDirectoryLoadCommand.NotifyCanExecuteChanged();
|
|
}
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>dotnet build --no-restore -warnaserror</automated>
|
|
</verify>
|
|
<done>All directory browse properties, commands, and change handlers exist. Build passes.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Implement LoadDirectoryAsync, CancelDirectoryLoad, and filter predicate</name>
|
|
<files>
|
|
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
|
|
</files>
|
|
<behavior>
|
|
- LoadDirectoryAsync fetches all users via IGraphUserDirectoryService.GetUsersAsync(clientId, includeGuests: true)
|
|
- Reports progress via DirectoryLoadStatus = $"Loading... {count} users"
|
|
- Populates DirectoryUsers on UI thread
|
|
- Sets IsLoadingDirectory true/false around the operation
|
|
- Handles cancellation (OperationCanceledException → sets status message)
|
|
- Handles errors (Exception → sets status message, logs)
|
|
- CancelDirectoryLoad cancels _directoryCts
|
|
- DirectoryFilterPredicate filters by DisplayName, UPN, Department, JobTitle (case-insensitive contains)
|
|
- When IncludeGuests is false, only shows users where UserType == "Member" (or UserType is null — defensive)
|
|
- When IncludeGuests is true, shows all users
|
|
- OnTenantSwitched clears DirectoryUsers, DirectoryFilterText, resets IsBrowseMode to false
|
|
</behavior>
|
|
<action>
|
|
1. Implement LoadDirectoryAsync:
|
|
```csharp
|
|
private async Task LoadDirectoryAsync()
|
|
{
|
|
if (_graphUserDirectoryService is null) return;
|
|
var clientId = _currentProfile?.ClientId;
|
|
if (string.IsNullOrEmpty(clientId))
|
|
{
|
|
StatusMessage = "No tenant profile selected. Please connect first.";
|
|
return;
|
|
}
|
|
|
|
_directoryCts?.Cancel();
|
|
_directoryCts?.Dispose();
|
|
_directoryCts = new CancellationTokenSource();
|
|
var ct = _directoryCts.Token;
|
|
|
|
IsLoadingDirectory = true;
|
|
DirectoryLoadStatus = "Loading...";
|
|
try
|
|
{
|
|
var progress = new Progress<int>(count =>
|
|
DirectoryLoadStatus = $"Loading... {count} users");
|
|
|
|
var users = await _graphUserDirectoryService.GetUsersAsync(
|
|
clientId, includeGuests: true, progress, ct);
|
|
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
var dispatcher = System.Windows.Application.Current?.Dispatcher;
|
|
if (dispatcher != null)
|
|
{
|
|
await dispatcher.InvokeAsync(() => PopulateDirectory(users));
|
|
}
|
|
else
|
|
{
|
|
PopulateDirectory(users);
|
|
}
|
|
|
|
DirectoryLoadStatus = $"{users.Count} users loaded";
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
DirectoryLoadStatus = "Load cancelled.";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
DirectoryLoadStatus = $"Failed: {ex.Message}";
|
|
_logger.LogError(ex, "Directory load failed.");
|
|
}
|
|
finally
|
|
{
|
|
IsLoadingDirectory = false;
|
|
}
|
|
}
|
|
|
|
private void PopulateDirectory(IReadOnlyList<GraphDirectoryUser> users)
|
|
{
|
|
DirectoryUsers.Clear();
|
|
foreach (var u in users)
|
|
DirectoryUsers.Add(u);
|
|
DirectoryUsersView.Refresh();
|
|
OnPropertyChanged(nameof(DirectoryUserCount));
|
|
}
|
|
```
|
|
|
|
2. Implement DirectoryFilterPredicate:
|
|
```csharp
|
|
private bool DirectoryFilterPredicate(object obj)
|
|
{
|
|
if (obj is not GraphDirectoryUser user) return false;
|
|
|
|
// Member/guest filter
|
|
if (!IncludeGuests && !string.Equals(user.UserType, "Member", StringComparison.OrdinalIgnoreCase))
|
|
return false;
|
|
|
|
// Text filter
|
|
if (string.IsNullOrWhiteSpace(DirectoryFilterText)) return true;
|
|
var filter = DirectoryFilterText.Trim();
|
|
return user.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)
|
|
|| user.UserPrincipalName.Contains(filter, StringComparison.OrdinalIgnoreCase)
|
|
|| (user.Department?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false)
|
|
|| (user.JobTitle?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false);
|
|
}
|
|
```
|
|
NOTE: When IncludeGuests is false, show users where UserType is "Member". Users with null UserType are excluded (defensive — should not happen with the updated select).
|
|
|
|
3. Update OnTenantSwitched — add directory state reset after existing code:
|
|
```csharp
|
|
// Directory browse mode reset
|
|
_directoryCts?.Cancel();
|
|
_directoryCts?.Dispose();
|
|
_directoryCts = null;
|
|
DirectoryUsers.Clear();
|
|
DirectoryFilterText = string.Empty;
|
|
DirectoryLoadStatus = string.Empty;
|
|
IsBrowseMode = false;
|
|
IsLoadingDirectory = false;
|
|
IncludeGuests = false;
|
|
OnPropertyChanged(nameof(DirectoryUserCount));
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>dotnet build --no-restore -warnaserror</automated>
|
|
</verify>
|
|
<done>LoadDirectoryAsync, filter predicate, and tenant switch cleanup implemented. Build passes.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 4: Write comprehensive tests for directory browse mode</name>
|
|
<files>
|
|
SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelDirectoryTests.cs
|
|
</files>
|
|
<behavior>
|
|
- Test 1: IsBrowseMode defaults to false
|
|
- Test 2: DirectoryUsers is empty by default
|
|
- Test 3: LoadDirectoryCommand exists and is not null
|
|
- Test 4: LoadDirectoryAsync populates DirectoryUsers with results from service
|
|
- Test 5: LoadDirectoryAsync reports progress via DirectoryLoadStatus
|
|
- Test 6: LoadDirectoryAsync with no profile sets StatusMessage and returns
|
|
- Test 7: CancelDirectoryLoadCommand cancels in-flight load
|
|
- Test 8: IncludeGuests=false filters out non-Member users in DirectoryUsersView
|
|
- Test 9: IncludeGuests=true shows all users in DirectoryUsersView
|
|
- Test 10: DirectoryFilterText filters by DisplayName
|
|
- Test 11: DirectoryFilterText filters by Department
|
|
- Test 12: DirectoryUsersView default sort is DisplayName ascending
|
|
- Test 13: OnTenantSwitched clears DirectoryUsers and resets IsBrowseMode
|
|
- Test 14: DirectoryUserCount reflects filtered count
|
|
- Test 15: Search mode properties (SearchQuery, SelectedUsers) still work (no regression)
|
|
</behavior>
|
|
<action>
|
|
1. Create `SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelDirectoryTests.cs`
|
|
|
|
2. Create helper factory similar to existing tests but also including IGraphUserDirectoryService mock:
|
|
```csharp
|
|
private static (UserAccessAuditViewModel vm,
|
|
Mock<IGraphUserDirectoryService> dirMock,
|
|
Mock<IUserAccessAuditService> auditMock)
|
|
CreateViewModel(IReadOnlyList<GraphDirectoryUser>? directoryResult = null)
|
|
{
|
|
var mockAudit = new Mock<IUserAccessAuditService>();
|
|
var mockGraph = new Mock<IGraphUserSearchService>();
|
|
var mockSession = new Mock<ISessionManager>();
|
|
var mockDir = new Mock<IGraphUserDirectoryService>();
|
|
mockDir.Setup(s => s.GetUsersAsync(
|
|
It.IsAny<string>(),
|
|
It.IsAny<bool>(),
|
|
It.IsAny<IProgress<int>>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(directoryResult ?? Array.Empty<GraphDirectoryUser>());
|
|
|
|
var vm = new UserAccessAuditViewModel(
|
|
mockAudit.Object, mockGraph.Object, mockSession.Object,
|
|
NullLogger<FeatureViewModelBase>.Instance,
|
|
graphUserDirectoryService: mockDir.Object);
|
|
vm._currentProfile = new TenantProfile { ... };
|
|
return (vm, mockDir, mockAudit);
|
|
}
|
|
```
|
|
|
|
3. Create test data helpers:
|
|
```csharp
|
|
private static GraphDirectoryUser MakeMember(string name = "Alice", string dept = "IT") =>
|
|
new(name, $"{name.ToLower()}@contoso.com", null, dept, "Engineer", "Member");
|
|
|
|
private static GraphDirectoryUser MakeGuest(string name = "Bob External") =>
|
|
new(name, $"{name.ToLower().Replace(" ", "")}@external.com", null, null, null, "Guest");
|
|
```
|
|
|
|
4. Write all tests. Use `[Trait("Category", "Unit")]`.
|
|
For LoadDirectoryAsync test: call the command via `vm.LoadDirectoryCommand.ExecuteAsync(null)` or expose an internal test method.
|
|
For ICollectionView filtering tests: add users to DirectoryUsers, set IncludeGuests/DirectoryFilterText, then check DirectoryUsersView.Cast<GraphDirectoryUser>().Count().
|
|
</action>
|
|
<verify>
|
|
<automated>dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~UserAccessAuditViewModelDirectory" --no-build -q</automated>
|
|
</verify>
|
|
<done>15+ tests covering all directory browse mode behavior. All pass.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
```bash
|
|
dotnet build --no-restore -warnaserror
|
|
dotnet test SharepointToolbox.Tests --filter "FullyQualifiedName~UserAccessAuditViewModel" --no-build -q
|
|
```
|
|
Both must pass. Existing UserAccessAuditViewModelTests must still pass (no regression).
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- SC1: IsBrowseMode toggle switches between Search and Browse modes; default is Search; no regression
|
|
- SC2: LoadDirectoryCommand fetches all users with progress reporting and cancellation support
|
|
- SC3: IncludeGuests toggle filters DirectoryUsersView in-memory without new Graph request
|
|
- SC4: DirectoryUsersView exposes DisplayName, UPN, Department, JobTitle; sorted by DisplayName
|
|
- IGraphUserDirectoryService injected via DI
|
|
- OnTenantSwitched clears all directory state
|
|
- 15+ tests covering all behaviors
|
|
- Build passes with zero warnings
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/13-user-directory-viewmodel/13-02-SUMMARY.md`
|
|
</output>
|