Files
Sharepoint-Toolbox/.planning/phases/07-user-access-audit/07-10-PLAN.md
2026-04-08 10:57:27 +02:00

6.6 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, gap_closure, source_gaps, must_haves
phase plan type wave depends_on files_modified autonomous requirements gap_closure source_gaps must_haves
07-user-access-audit 10 execute 6
07-08
SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs
true
UACC-01
true
Gap 3: Debounced search test absent (Plan 08 truth partially unmet)
truths artifacts key_links
A unit test verifies that setting SearchQuery to a value of length >= 2 triggers IGraphUserSearchService.SearchUsersAsync after the debounce delay
path provides contains
SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs Debounced search unit test SearchQuery_debounced_calls_SearchUsersAsync
from to via pattern
SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs Tests SearchQuery property change → DebounceSearchAsync → SearchUsersAsync 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.

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

@.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 }


<!-- 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.
public interface IGraphUserSearchService
{
    Task<IReadOnlyList<GraphUserResult>> 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<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`