172 lines
6.6 KiB
Markdown
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>
|