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

172 lines
6.6 KiB
Markdown

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