docs: start milestone v2.2 Report Branding & User Directory
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
131
.planning/phases/06-global-site-selection/06-CONTEXT.md
Normal file
131
.planning/phases/06-global-site-selection/06-CONTEXT.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Phase 6: Global Site Selection - Context
|
||||
|
||||
**Gathered:** 2026-04-07
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Administrators can select one or more target sites once from the toolbar and have every feature tab use that selection by default — eliminating the need to re-enter site URLs on each tab. Individual tabs can override the global selection without clearing the global state.
|
||||
|
||||
Requirements: SITE-01, SITE-02
|
||||
|
||||
Success Criteria:
|
||||
1. A multi-site picker control is visible in the main toolbar at all times, regardless of which tab is active
|
||||
2. Selecting sites in the toolbar causes all feature tabs to default to those sites when an operation is run
|
||||
3. A user can override the global selection on any individual tab without clearing the global state
|
||||
4. The global site selection persists across tab switches within the same session
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Toolbar site picker placement
|
||||
- Add a "Select Sites" button to the existing ToolBar (after the Clear Session button, separated by a Separator)
|
||||
- Next to the button, show a summary label: "3 site(s) selected" or "No sites selected"
|
||||
- Clicking the button opens the existing SitePickerDialog pattern (reuse from PermissionsViewModel)
|
||||
- The picker requires a connected tenant (button disabled when no profile is connected)
|
||||
|
||||
### Global selection broadcast
|
||||
- Create a new `GlobalSitesChangedMessage` (ValueChangedMessage<IReadOnlyList<SiteInfo>>) sent via WeakReferenceMessenger when the toolbar selection changes
|
||||
- `MainWindowViewModel` owns the global site selection state: `ObservableCollection<SiteInfo> GlobalSelectedSites`
|
||||
- On tenant switch, clear the global selection (sites belong to a tenant)
|
||||
|
||||
### Tab consumption of global selection
|
||||
- `FeatureViewModelBase` registers for `GlobalSitesChangedMessage` in `OnActivated()` and stores the global sites in a protected property `IReadOnlyList<SiteInfo> GlobalSites`
|
||||
- Each tab's `RunOperationAsync` checks: if local override sites exist, use those; else if GlobalSites is non-empty, use those; else fall back to the SiteUrl text box
|
||||
- The SiteUrl TextBox on each tab shows a placeholder/hint when global sites are active (e.g., "Using 3 globally selected sites" as watermark text)
|
||||
|
||||
### Local override behavior
|
||||
- Tabs that already have per-tab site pickers (like Permissions) keep them
|
||||
- When a user picks sites locally on a tab, that overrides the global selection for that tab only
|
||||
- A "Clear local selection" action resets the tab back to using global sites
|
||||
- The global selection in the toolbar is never modified by per-tab overrides
|
||||
|
||||
### Tabs that DO NOT consume global sites
|
||||
- Settings tab: no site URL needed
|
||||
- Bulk Sites tab: creates sites from CSV, does not target existing sites
|
||||
- Templates tab (apply): creates a new site, does not target existing sites
|
||||
|
||||
### Tabs that consume global sites (single-site)
|
||||
- Storage, Search, Duplicates, Folder Structure: these currently take a single SiteUrl
|
||||
- When global sites are selected, these tabs use the first site in the global list by default
|
||||
- The SiteUrl TextBox is pre-filled with the first global site URL (user can change it = local override)
|
||||
|
||||
### Tabs that consume global sites (multi-site)
|
||||
- Permissions: already supports multi-site; global sites pre-populate its SelectedSites collection
|
||||
- Transfer: source site pre-filled from first global site
|
||||
|
||||
### Claude's Discretion
|
||||
- Exact XAML layout of the toolbar site picker button and label
|
||||
- Whether to refactor SitePickerDialog or reuse as-is from MainWindow code-behind
|
||||
- Internal naming of properties and helper methods
|
||||
- Whether to add a chip/tag display for selected sites or keep it as a count label
|
||||
- Localization key names for new strings
|
||||
|
||||
</decisions>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `SitePickerDialog` (Views/Dialogs/): Filterable checkbox list of sites with Select All/Deselect All — loads from `ISiteListService.GetSitesAsync()`. Currently only wired from PermissionsView; needs to be wired from MainWindow toolbar too.
|
||||
- `SiteInfo(string Url, string Title)` record (Core/Models/): Already used by SitePickerDialog and PermissionsViewModel
|
||||
- `ISiteListService.GetSitesAsync(TenantProfile, progress, ct)`: Enumerates all sites in a tenant. Already registered in DI.
|
||||
- `TenantSwitchedMessage`: Broadcast pattern for tenant changes — global site selection follows the same pattern
|
||||
- `WeakReferenceMessenger`: Already used for TenantSwitched and ProgressUpdated messages
|
||||
- `FeatureViewModelBase.OnActivated()`: Already registers for TenantSwitchedMessage — extend to also register for GlobalSitesChangedMessage
|
||||
|
||||
### Established Patterns
|
||||
- Dialog factories set on ViewModels as `Func<Window>?` from View code-behind (keeps Window refs out of VMs)
|
||||
- `[ObservableProperty]` for bindable state
|
||||
- `ObservableCollection<T>` for list-bound UI elements
|
||||
- Tab content resolved from DI in MainWindow.xaml.cs
|
||||
- Localization via `TranslationSource.Instance["key"]` with Strings.resx / Strings.fr.resx
|
||||
|
||||
### Integration Points
|
||||
- `MainWindow.xaml`: Add site picker button + label to ToolBar
|
||||
- `MainWindowViewModel.cs`: Add GlobalSelectedSites, OpenGlobalSitePickerCommand, GlobalSitesChangedMessage broadcast
|
||||
- `MainWindow.xaml.cs`: Wire SitePickerDialog factory for the toolbar (same pattern as PermissionsView)
|
||||
- `FeatureViewModelBase.cs`: Register for GlobalSitesChangedMessage, add GlobalSites property
|
||||
- `Core/Messages/`: New GlobalSitesChangedMessage class
|
||||
- Each tab ViewModel: Update RunOperationAsync to check GlobalSites before falling back to SiteUrl
|
||||
- `Strings.resx` / `Strings.fr.resx`: New localization keys for toolbar site picker
|
||||
- `App.xaml.cs`: No new DI registrations needed (SitePickerDialog factory and ISiteListService already registered)
|
||||
|
||||
### Key Files
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `MainWindow.xaml` | Toolbar XAML — add site picker controls |
|
||||
| `MainWindowViewModel.cs` | Global selection state + command |
|
||||
| `MainWindow.xaml.cs` | Wire SitePickerDialog factory for toolbar |
|
||||
| `FeatureViewModelBase.cs` | Base class — receive global sites message |
|
||||
| `Core/Messages/TenantSwitchedMessage.cs` | Pattern reference for new message |
|
||||
| `Views/Dialogs/SitePickerDialog.xaml.cs` | Reuse as-is |
|
||||
| `ViewModels/Tabs/PermissionsViewModel.cs` | Already has multi-site pattern — adapt to consume global sites |
|
||||
| `ViewModels/Tabs/StorageViewModel.cs` | Single-site pattern — adapt to consume global sites |
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- The toolbar site count label should update live when sites are selected/deselected
|
||||
- When no tenant is connected, the "Select Sites" button should be disabled with a tooltip explaining why
|
||||
- Clearing the session (Clear Session button) should also clear the global site selection
|
||||
- The global selection should survive tab switching (it lives on MainWindowViewModel, not on any tab)
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
None — all items are within phase scope
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 06-global-site-selection*
|
||||
*Context gathered: 2026-04-07*
|
||||
163
.planning/phases/07-user-access-audit/07-09-PLAN.md
Normal file
163
.planning/phases/07-user-access-audit/07-09-PLAN.md
Normal file
@@ -0,0 +1,163 @@
|
||||
---
|
||||
phase: 07-user-access-audit
|
||||
plan: 09
|
||||
type: execute
|
||||
wave: 6
|
||||
depends_on: ["07-05"]
|
||||
files_modified:
|
||||
- SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml
|
||||
autonomous: true
|
||||
requirements:
|
||||
- UACC-01
|
||||
- UACC-02
|
||||
gap_closure: true
|
||||
source_gaps:
|
||||
- "Gap 1: Missing DataGrid visual indicators (guest badge + warning icon)"
|
||||
- "Gap 2: Missing ObjectType column in DataGrid"
|
||||
must_haves:
|
||||
truths:
|
||||
- "High-privilege entries show a warning icon (⚠) in the Permission Level column cell template"
|
||||
- "External users show a guest badge (👤 Guest) in the User column cell template when IsExternalUser is true"
|
||||
- "DataGrid columns include Object Type bound to ObjectType between Object and Permission Level"
|
||||
artifacts:
|
||||
- path: "SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml"
|
||||
provides: "DataGrid with visual indicators for high-privilege/external users and ObjectType column"
|
||||
contains: "IsExternalUser DataTrigger, IsHighPrivilege warning icon, ObjectType column"
|
||||
key_links:
|
||||
- from: "SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml"
|
||||
to: "SharepointToolbox/Core/Models/UserAccessEntry.cs"
|
||||
via: "Bindings on IsExternalUser, IsHighPrivilege, ObjectType properties"
|
||||
pattern: "DataTrigger Binding"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add missing visual indicators and ObjectType column to the UserAccessAuditView DataGrid.
|
||||
|
||||
Purpose: Close verification gaps 1 and 2 — the XAML currently lacks per-row guest badges for external users, warning icons for high-privilege entries, and the ObjectType column.
|
||||
Output: Updated UserAccessAuditView.xaml with all three additions.
|
||||
</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-05-SUMMARY.md
|
||||
@.planning/phases/07-user-access-audit/07-VERIFICATION.md
|
||||
|
||||
<interfaces>
|
||||
<!-- UserAccessEntry fields available for binding -->
|
||||
From SharepointToolbox/Core/Models/UserAccessEntry.cs:
|
||||
```csharp
|
||||
public record UserAccessEntry(
|
||||
string UserDisplayName, string UserLogin,
|
||||
string SiteUrl, string SiteTitle,
|
||||
string ObjectType, string ObjectTitle, string ObjectUrl,
|
||||
string PermissionLevel, AccessType AccessType, string GrantedThrough,
|
||||
bool IsHighPrivilege, bool IsExternalUser);
|
||||
```
|
||||
|
||||
<!-- Current DataGrid columns (lines 219-249 of UserAccessAuditView.xaml) -->
|
||||
Current columns: User (UserLogin), Site (SiteTitle), Object (ObjectTitle), Permission Level (PermissionLevel), Access Type (template), Granted Through (GrantedThrough).
|
||||
Missing: ObjectType column, guest badge in User column, warning icon in Permission Level column.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add guest badge, warning icon, and ObjectType column to DataGrid</name>
|
||||
<files>SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml</files>
|
||||
<action>
|
||||
Modify the DataGrid columns section (lines 219-249) with three changes:
|
||||
|
||||
**Change 1 — Convert User column to DataGridTemplateColumn with guest badge:**
|
||||
Replace the plain `DataGridTextColumn Header="User"` with a `DataGridTemplateColumn`:
|
||||
```xml
|
||||
<DataGridTemplateColumn Header="User" Width="180">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding UserLogin}" VerticalAlignment="Center" />
|
||||
<Border Background="#F39C12" CornerRadius="3" Padding="4,1" Margin="6,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsExternalUser}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="Guest" FontSize="10" Foreground="White" FontWeight="SemiBold" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
```
|
||||
|
||||
**Change 2 — Convert Permission Level column to DataGridTemplateColumn with warning icon:**
|
||||
Replace the plain `DataGridTextColumn Header="Permission Level"` with a `DataGridTemplateColumn`:
|
||||
```xml
|
||||
<DataGridTemplateColumn Header="Permission Level" Width="140">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="⚠" Foreground="#E74C3C" Margin="0,0,4,0"
|
||||
FontSize="12" VerticalAlignment="Center">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsHighPrivilege}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding PermissionLevel}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
```
|
||||
|
||||
**Change 3 — Add ObjectType column between Object and Permission Level:**
|
||||
```xml
|
||||
<DataGridTextColumn Header="Object Type" Binding="{Binding ObjectType}" Width="90" />
|
||||
```
|
||||
|
||||
Insert this column after the "Object" column and before the "Permission Level" column.
|
||||
|
||||
Final column order: User (with guest badge), Site, Object, Object Type, Permission Level (with ⚠ icon), Access Type, Granted Through.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>DataGrid now shows: guest badge on external user rows (orange "Guest" pill), warning icon (⚠) on high-privilege permission levels, and ObjectType column showing Site Collection/Site/List/Folder distinction.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `dotnet build SharepointToolbox/SharepointToolbox.csproj` — XAML compiles without errors
|
||||
- Visual inspection: DataGrid columns order is User (with guest badge), Site, Object, Object Type, Permission Level (with ⚠), Access Type, Granted Through
|
||||
- Guest badge visible only when IsExternalUser=true
|
||||
- Warning icon visible only when IsHighPrivilege=true
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
The DataGrid shows guest badges for external users, warning icons for high-privilege entries, and the ObjectType column — closing verification gaps 1 and 2.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/07-user-access-audit/07-09-SUMMARY.md`
|
||||
</output>
|
||||
171
.planning/phases/07-user-access-audit/07-10-PLAN.md
Normal file
171
.planning/phases/07-user-access-audit/07-10-PLAN.md
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user