12 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-user-access-audit | 08 | execute | 5 |
|
|
true |
|
|
Purpose: Verify the critical behavior of user filtering, access type classification, export formatting, and ViewModel orchestration. Output: 4 test files covering services, exports, and ViewModel
<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-02-SUMMARY.md @.planning/phases/07-user-access-audit/07-04-SUMMARY.md @.planning/phases/07-user-access-audit/07-06-SUMMARY.md From SharepointToolbox/Services/UserAccessAuditService.cs: ```csharp public class UserAccessAuditService : IUserAccessAuditService { public UserAccessAuditService(IPermissionsService permissionsService) { } public async Task> AuditUsersAsync( ISessionManager sessionManager, IReadOnlyList targetUserLogins, IReadOnlyList sites, ScanOptions options, IProgress progress, CancellationToken ct); } ```From SharepointToolbox/Services/Export/UserAccessCsvExportService.cs:
public class UserAccessCsvExportService
{
public string BuildCsv(string userDisplayName, string userLogin, IReadOnlyList<UserAccessEntry> entries);
public async Task WriteSingleFileAsync(IReadOnlyList<UserAccessEntry> entries, string filePath, CancellationToken ct);
}
From SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs:
public class UserAccessHtmlExportService
{
public string BuildHtml(IReadOnlyList<UserAccessEntry> entries);
public async Task WriteAsync(IReadOnlyList<UserAccessEntry> entries, string filePath, CancellationToken ct);
}
From SharepointToolbox.Tests (uses xUnit + NSubstitute):
using NSubstitute;
using Xunit;
var mockPermService = Substitute.For<IPermissionsService>();
var mockSessionMgr = Substitute.For<ISessionManager>();
mockSessionMgr.GetOrCreateContextAsync(Arg.Any<TenantProfile>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult<ClientContext>(null!)); // service creates context, tests mock it
mockPermService.ScanSiteAsync(Arg.Any<ClientContext>(), Arg.Any<ScanOptions>(), Arg.Any<IProgress<OperationProgress>>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<PermissionEntry>>(testEntries));
Test cases for AuditUsersAsync:
1. **Filters_by_target_user_login**: Mock IPermissionsService returning entries for 3 users. Audit for 1 user. Assert only that user's entries returned.
2. **Matches_user_by_email_in_claim_format**: PermissionEntry.UserLogins = "i:0#.f|membership|alice@contoso.com". Target = "alice@contoso.com". Assert match found.
3. **Classifies_direct_access**: Entry with HasUniquePermissions=true, GrantedThrough="Direct Permissions". Assert AccessType.Direct.
4. **Classifies_group_access**: Entry with HasUniquePermissions=true, GrantedThrough="SharePoint Group: Members". Assert AccessType.Group.
5. **Classifies_inherited_access**: Entry with HasUniquePermissions=false. Assert AccessType.Inherited.
6. **Detects_high_privilege**: Entry with PermissionLevels="Full Control". Assert IsHighPrivilege=true.
7. **Detects_high_privilege_site_admin**: Entry with PermissionLevels="Site Collection Administrator". Assert IsHighPrivilege=true.
8. **Flags_external_user**: Entry with UserLogins containing "#EXT#". Assert IsExternalUser=true.
9. **Splits_semicolon_users**: Entry with Users="Alice;Bob", UserLogins="alice@x.com;bob@x.com". Target both. Assert 2 separate UserAccessEntry rows per permission level.
10. **Splits_semicolon_permission_levels**: Entry with PermissionLevels="Read;Contribute". Assert 2 UserAccessEntry rows (one per level).
11. **Empty_targets_returns_empty**: Pass empty targetUserLogins. Assert empty result.
12. **Scans_multiple_sites**: Pass 2 sites. Assert both site entries appear in results.
Mock setup pattern:
```csharp
private static PermissionEntry MakeEntry(
string users = "Alice", string logins = "alice@contoso.com",
string levels = "Read", string grantedThrough = "Direct Permissions",
bool hasUnique = true, string objectType = "List", string title = "Docs",
string url = "https://contoso.sharepoint.com/Docs",
string principalType = "User") =>
new(objectType, title, url, hasUnique, users, logins, levels, grantedThrough, principalType);
```
For the SessionManager mock, the service passes TenantProfile objects to GetOrCreateContextAsync. The mock should return null for ClientContext since the PermissionsService is also mocked (it never actually uses the context in tests).
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~UserAccessAuditServiceTests" --no-build 2>&1 | tail -10
All UserAccessAuditService tests pass: user filtering, claim format matching, access type classification (Direct/Group/Inherited), high-privilege detection, external user flagging, semicolon splitting, multi-site scanning.
Task 2: Write export service and ViewModel tests
SharepointToolbox.Tests/Services/Export/UserAccessCsvExportServiceTests.cs, SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs, SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs
**UserAccessCsvExportServiceTests.cs**:
1. **BuildCsv_includes_summary_section**: Assert output starts with "User Access Audit Report" and includes user name, total, sites count.
2. **BuildCsv_includes_data_header**: Assert DataHeader line present after summary.
3. **BuildCsv_escapes_quotes**: Entry with title containing double quotes. Assert RFC 4180 escaping.
4. **BuildCsv_correct_column_count**: Assert each data row has 7 comma-separated fields.
5. **WriteSingleFileAsync_includes_all_users**: Pass entries for 2 users. Assert both appear in output.
**UserAccessHtmlExportServiceTests.cs**:
1. **BuildHtml_contains_doctype**: Assert starts with "<!DOCTYPE html>".
2. **BuildHtml_has_stats_cards**: Assert contains "Total Accesses" and stat-card CSS class.
3. **BuildHtml_has_both_views**: Assert contains "by-user" and "by-site" div/section identifiers.
4. **BuildHtml_has_access_type_badges**: Assert contains "access-direct", "access-group", "access-inherited" CSS classes.
5. **BuildHtml_has_filter_script**: Assert contains "filterTable" JS function.
6. **BuildHtml_has_toggle_script**: Assert contains "toggleView" JS function.
7. **BuildHtml_encodes_html_entities**: Entry with title containing "<script>". Assert encoded as "<script>".
**UserAccessAuditViewModelTests.cs** (use test constructor, mock services):
1. **RunOperation_calls_AuditUsersAsync**: Mock IUserAccessAuditService, add selected user + site, run. Assert AuditUsersAsync was called.
2. **RunOperation_populates_results**: Mock returns entries. Assert Results.Count matches.
3. **RunOperation_updates_summary_properties**: Assert TotalAccessCount, SitesCount, HighPrivilegeCount computed correctly.
4. **OnTenantSwitched_resets_state**: Set results and selected users, switch tenant. Assert all cleared.
5. **OnGlobalSitesChanged_updates_selected_sites**: Send GlobalSitesChangedMessage. Assert SelectedSites updated.
6. **OnGlobalSitesChanged_skipped_when_override**: Set _hasLocalSiteOverride. Send message. Assert SelectedSites unchanged.
7. **CanExport_false_when_no_results**: Assert ExportCsvCommand.CanExecute is false when Results is empty.
8. **CanExport_true_when_has_results**: Add results. Assert ExportCsvCommand.CanExecute is true.
For ViewModel tests, use the internal test constructor (no export services). Mock IUserAccessAuditService, IGraphUserSearchService, ISessionManager. Use NSubstitute.
Note: ViewModel tests that call RunOperationAsync should use the internal TestRunOperationAsync pattern from PermissionsViewModel (if exposed), or invoke RunCommand.ExecuteAsync directly.
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~UserAccess" 2>&1 | tail -15
All Phase 7 tests pass: 12 audit service tests, 7 CSV export tests, 7 HTML export tests, 8 ViewModel tests. Total ~34 tests covering core business logic, export formatting, and ViewModel orchestration.
- `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~UserAccess"` — all pass
- `dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj` — no regressions in existing tests
- Test coverage: user filtering, access classification, export format, ViewModel lifecycle
<success_criteria> All Phase 7 unit tests pass. Critical business logic is verified: user login matching (including claim format), access type classification, high-privilege/external detection, CSV/HTML export format, and ViewModel state management. No regressions in existing tests. </success_criteria>
After completion, create `.planning/phases/07-user-access-audit/07-08-SUMMARY.md`