---
phase: 07-user-access-audit
plan: 10
type: execute
wave: 6
depends_on: ["07-08"]
files_modified:
- SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs
autonomous: true
requirements:
- UACC-01
gap_closure: true
source_gaps:
- "Gap 3: Debounced search test absent (Plan 08 truth partially unmet)"
must_haves:
truths:
- "A unit test verifies that setting SearchQuery to a value of length >= 2 triggers IGraphUserSearchService.SearchUsersAsync after the debounce delay"
artifacts:
- path: "SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs"
provides: "Debounced search unit test"
contains: "SearchQuery_debounced_calls_SearchUsersAsync"
key_links:
- from: "SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs"
to: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
via: "Tests SearchQuery property change → DebounceSearchAsync → SearchUsersAsync"
pattern: "SearchUsersAsync"
---
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.
@C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/dev/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/07-user-access-audit/07-CONTEXT.md
@.planning/phases/07-user-access-audit/07-08-SUMMARY.md
@.planning/phases/07-user-access-audit/07-VERIFICATION.md
```csharp
// Line 281-290: OnSearchQueryChanged triggers DebounceSearchAsync
partial void OnSearchQueryChanged(string value)
{
_searchCts?.Cancel();
_searchCts?.Dispose();
_searchCts = new CancellationTokenSource();
var ct = _searchCts.Token;
_ = DebounceSearchAsync(value, ct);
}
// 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
}
```
```csharp
// Uses Moq + xUnit. CreateViewModel helper returns (vm, auditMock).
// mockGraph is Mock created inside CreateViewModel.
// The test needs access to mockGraph — may need to extend CreateViewModel to return it.
```
```csharp
public interface IGraphUserSearchService
{
Task> SearchUsersAsync(
string clientId, string query, int maxResults, CancellationToken ct);
}
```
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
{
new("Alice Smith", "alice@contoso.com", "alice@contoso.com")
};
var (vm, _, graphMock) = CreateViewModel();
graphMock
.Setup(s => s.SearchUsersAsync(
It.IsAny(),
It.Is(q => q == "Ali"),
It.IsAny(),
It.IsAny()))
.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(),
"Ali",
It.IsAny(),
It.IsAny()),
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
The debounced search path (SearchQuery → 300ms delay → SearchUsersAsync) has unit test coverage, closing verification gap 3.