---
phase: 07-user-access-audit
plan: 08
type: execute
wave: 5
depends_on: ["07-02", "07-03", "07-04", "07-06"]
files_modified:
  - SharepointToolbox.Tests/Services/UserAccessAuditServiceTests.cs
  - SharepointToolbox.Tests/Services/Export/UserAccessCsvExportServiceTests.cs
  - SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs
  - SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs
autonomous: true
requirements:
  - UACC-01
  - UACC-02
must_haves:
  truths:
    - "UserAccessAuditService tests verify: user filtering, access type classification, high-privilege detection, external user detection, multi-user splitting"
    - "CSV export tests verify: summary section presence, correct column count, RFC 4180 escaping, per-user file naming"
    - "HTML export tests verify: contains stats cards, both view sections, access type badges, filter script"
    - "ViewModel tests verify: debounced search triggers service, run audit populates results, tenant switch resets state, global sites override pattern"
  artifacts:
    - path: "SharepointToolbox.Tests/Services/UserAccessAuditServiceTests.cs"
      provides: "Unit tests for audit service business logic"
      contains: "UserAccessAuditServiceTests"
    - path: "SharepointToolbox.Tests/Services/Export/UserAccessCsvExportServiceTests.cs"
      provides: "Unit tests for CSV export"
      contains: "UserAccessCsvExportServiceTests"
    - path: "SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs"
      provides: "Unit tests for HTML export"
      contains: "UserAccessHtmlExportServiceTests"
    - path: "SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs"
      provides: "Unit tests for ViewModel logic"
      contains: "UserAccessAuditViewModelTests"
  key_links:
    - from: "SharepointToolbox.Tests/Services/UserAccessAuditServiceTests.cs"
      to: "SharepointToolbox/Services/UserAccessAuditService.cs"
      via: "Tests TransformEntries logic with mock IPermissionsService"
      pattern: "AuditUsersAsync"
---

<objective>
Write unit tests for the core Phase 7 business logic: UserAccessAuditService (filtering, classification), export services (CSV/HTML output), and ViewModel (search, audit, state management).

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
</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-02-SUMMARY.md
@.planning/phases/07-user-access-audit/07-04-SUMMARY.md
@.planning/phases/07-user-access-audit/07-06-SUMMARY.md

<interfaces>
<!-- From 07-01/07-02: Service under test -->
From SharepointToolbox/Services/UserAccessAuditService.cs:
```csharp
public class UserAccessAuditService : IUserAccessAuditService
{
    public UserAccessAuditService(IPermissionsService permissionsService) { }
    public async Task<IReadOnlyList<UserAccessEntry>> AuditUsersAsync(
        ISessionManager sessionManager,
        IReadOnlyList<string> targetUserLogins,
        IReadOnlyList<SiteInfo> sites,
        ScanOptions options,
        IProgress<OperationProgress> progress,
        CancellationToken ct);
}
```

<!-- From 07-06: Export services under test -->
From SharepointToolbox/Services/Export/UserAccessCsvExportService.cs:
```csharp
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:
```csharp
public class UserAccessHtmlExportService
{
    public string BuildHtml(IReadOnlyList<UserAccessEntry> entries);
    public async Task WriteAsync(IReadOnlyList<UserAccessEntry> entries, string filePath, CancellationToken ct);
}
```

<!-- Existing test patterns -->
From SharepointToolbox.Tests (uses xUnit + NSubstitute):
```csharp
using NSubstitute;
using Xunit;
```

<!-- Mock patterns for IPermissionsService, ISessionManager -->
```csharp
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));
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Write UserAccessAuditService unit tests</name>
  <files>SharepointToolbox.Tests/Services/UserAccessAuditServiceTests.cs</files>
  <action>
    Create `SharepointToolbox.Tests/Services/UserAccessAuditServiceTests.cs` with xUnit + NSubstitute.

    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).
  </action>
  <verify>
    <automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~UserAccessAuditServiceTests" --no-build 2>&1 | tail -10</automated>
  </verify>
  <done>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.</done>
</task>

<task type="auto">
  <name>Task 2: Write export service and ViewModel tests</name>
  <files>SharepointToolbox.Tests/Services/Export/UserAccessCsvExportServiceTests.cs, SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs, SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs</files>
  <action>
    **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 "&lt;script&gt;".

    **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.
  </action>
  <verify>
    <automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~UserAccess" 2>&1 | tail -15</automated>
  </verify>
  <done>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.</done>
</task>

</tasks>

<verification>
- `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
</verification>

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

<output>
After completion, create `.planning/phases/07-user-access-audit/07-08-SUMMARY.md`
</output>
