Add a unit test for the debounced search path in UserAccessAuditViewModel.
Purpose: Close verification gap 3 — plan 08 required "ViewModel tests verify: debounced search triggers service" but no such test exists.
Output: One new test method added to UserAccessAuditViewModelTests.cs.
// Line 406-458: DebounceSearchAsync waits 300ms then calls SearchUsersAsync
private async Task DebounceSearchAsync(string query, CancellationToken ct)
{
await Task.Delay(300, ct);
// ... guard: query null/whitespace or < 2 chars → clear and return
var clientId = _currentProfile?.ClientId ?? string.Empty;
var results = await _graphUserSearchService.SearchUsersAsync(clientId, query, 10, ct);
// ... dispatches results to SearchResults collection
}
<!-- Existing test patterns (from UserAccessAuditViewModelTests.cs) -->
```csharp
// Uses Moq + xUnit. CreateViewModel helper returns (vm, auditMock).
// mockGraph is Mock<IGraphUserSearchService> created inside CreateViewModel.
// The test needs access to mockGraph — may need to extend CreateViewModel to return it.
Task 1: Add debounced search unit test
SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs
**Step 1**: Extend the `CreateViewModel` helper to also return the `Mock` so tests can set up expectations and verify calls on it. Change the return tuple from `(vm, auditMock)` to `(vm, auditMock, graphMock)`. Update all 8 existing test calls to destructure the third element (use `_` discard).
**Step 2**: Add the following test method after Test 8:
```csharp
// ── Test 9: Debounced search triggers SearchUsersAsync ──────────────
[Fact]
public async Task SearchQuery_debounced_calls_SearchUsersAsync()
{
var graphResults = new List<GraphUserResult>
{
new("Alice Smith", "alice@contoso.com", "alice@contoso.com")
};
var (vm, _, graphMock) = CreateViewModel();
graphMock
.Setup(s => s.SearchUsersAsync(
It.IsAny<string>(),
It.Is<string>(q => q == "Ali"),
It.IsAny<int>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(graphResults);
// Set a TenantProfile so _currentProfile is non-null
var profile = new TenantProfile
{
Name = "Test",
TenantUrl = "https://contoso.sharepoint.com",
ClientId = "test-client-id"
};
WeakReferenceMessenger.Default.Send(new TenantSwitchedMessage(profile));
// Act: set SearchQuery which triggers OnSearchQueryChanged → DebounceSearchAsync
vm.SearchQuery = "Ali";
// Wait longer than 300ms debounce to allow async fire-and-forget to complete
await Task.Delay(600);
// Assert: SearchUsersAsync was called with the query
graphMock.Verify(
s => s.SearchUsersAsync(
It.IsAny<string>(),
"Ali",
It.IsAny<int>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
```
**Important notes:**
- The `DebounceSearchAsync` method uses `Application.Current?.Dispatcher` which will be null in tests. The else branch (lines 438-442) handles this by adding directly to SearchResults — this is the test-safe path.
- The 600ms delay in the test ensures the 300ms debounce + async execution has time to complete.
- The TenantSwitchedMessage sets `_currentProfile` so that `_currentProfile?.ClientId` is non-null.
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~UserAccessAuditViewModelTests" 2>&1 | tail -10
Debounced search test passes: setting SearchQuery to "Ali" triggers SearchUsersAsync after the 300ms debounce. All 9 ViewModel tests pass with no regressions.
- `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~UserAccessAuditViewModelTests"` — all 9 tests pass
- `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj` — no regressions
<success_criteria>
The debounced search path (SearchQuery → 300ms delay → SearchUsersAsync) has unit test coverage, closing verification gap 3.
</success_criteria>
After completion, create `.planning/phases/07-user-access-audit/07-10-SUMMARY.md`