---
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"
---

<objective>
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.
</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/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

<interfaces>
<!-- ViewModel debounce path (from UserAccessAuditViewModel.cs) -->
```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
}
```

<!-- 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.
```

<!-- IGraphUserSearchService contract -->
```csharp
public interface IGraphUserSearchService
{
    Task<IReadOnlyList<GraphUserResult>> SearchUsersAsync(
        string clientId, string query, int maxResults, CancellationToken ct);
}
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add debounced search unit test</name>
  <files>SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs</files>
  <action>
    **Step 1**: Extend the `CreateViewModel` helper to also return the `Mock<IGraphUserSearchService>` 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.
  </action>
  <verify>
    <automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~UserAccessAuditViewModelTests" 2>&1 | tail -10</automated>
  </verify>
  <done>Debounced search test passes: setting SearchQuery to "Ali" triggers SearchUsersAsync after the 300ms debounce. All 9 ViewModel tests pass with no regressions.</done>
</task>

</tasks>

<verification>
- `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~UserAccessAuditViewModelTests"` — all 9 tests pass
- `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj` — no regressions
</verification>

<success_criteria>
The debounced search path (SearchQuery → 300ms delay → SearchUsersAsync) has unit test coverage, closing verification gap 3.
</success_criteria>

<output>
After completion, create `.planning/phases/07-user-access-audit/07-10-SUMMARY.md`
</output>
