--- phase: 15-consolidation-data-model plan: 02 type: execute wave: 2 depends_on: - 15-01 files_modified: - SharepointToolbox.Tests/Helpers/PermissionConsolidatorTests.cs autonomous: true requirements: - RPT-04 must_haves: truths: - "Empty input returns empty list" - "Single entry produces 1 consolidated row with 1 location" - "3 entries with same key produce 1 row with 3 locations" - "Entries with different keys remain separate rows" - "Key matching is case-insensitive" - "MakeKey produces expected pipe-delimited format" - "10-row input with 3 duplicate pairs produces 7 rows" - "LocationCount matches Locations.Count" - "IsHighPrivilege and IsExternalUser are preserved from first entry" - "Existing solution builds with no compilation errors" artifacts: - path: "SharepointToolbox.Tests/Helpers/PermissionConsolidatorTests.cs" provides: "Unit tests for PermissionConsolidator" min_lines: 120 key_links: - from: "SharepointToolbox.Tests/Helpers/PermissionConsolidatorTests.cs" to: "SharepointToolbox/Core/Helpers/PermissionConsolidator.cs" via: "calls Consolidate and MakeKey (internal via InternalsVisibleTo)" pattern: "PermissionConsolidator\\.(Consolidate|MakeKey)" - from: "SharepointToolbox.Tests/Helpers/PermissionConsolidatorTests.cs" to: "SharepointToolbox/Core/Models/UserAccessEntry.cs" via: "constructs test UserAccessEntry instances" pattern: "new UserAccessEntry" --- Create comprehensive unit tests for the PermissionConsolidator service and verify the full solution builds cleanly. Purpose: Prove that the consolidation logic handles all edge cases (empty input, single entry, merging, case-insensitivity, the 10-row/7-output scenario from requirements) and that adding the new files does not break existing code. Output: One test file with 10 test methods covering all RPT-04 test requirements, plus a clean full-solution build. @C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/dev/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/15-consolidation-data-model/15-CONTEXT.md @.planning/phases/15-consolidation-data-model/15-RESEARCH.md @.planning/phases/15-consolidation-data-model/15-01-SUMMARY.md ```csharp // SharepointToolbox/Core/Models/LocationInfo.cs namespace SharepointToolbox.Core.Models; public record LocationInfo(string SiteUrl, string SiteTitle, string ObjectTitle, string ObjectUrl, string ObjectType); // SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs namespace SharepointToolbox.Core.Models; public record ConsolidatedPermissionEntry( string UserDisplayName, string UserLogin, string PermissionLevel, AccessType AccessType, string GrantedThrough, bool IsHighPrivilege, bool IsExternalUser, IReadOnlyList Locations ) { public int LocationCount => Locations.Count; } // SharepointToolbox/Core/Helpers/PermissionConsolidator.cs namespace SharepointToolbox.Core.Helpers; public static class PermissionConsolidator { internal static string MakeKey(UserAccessEntry entry); public static IReadOnlyList Consolidate(IReadOnlyList entries); } // SharepointToolbox/Core/Models/UserAccessEntry.cs namespace SharepointToolbox.Core.Models; public enum AccessType { Direct, Group, Inherited } 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 ); ``` Task 1: Create PermissionConsolidatorTests with all 10 test cases SharepointToolbox.Tests/Helpers/PermissionConsolidatorTests.cs - RPT-04-a: Empty input returns empty list - RPT-04-b: Single entry produces 1 consolidated row with 1 location - RPT-04-c: 3 entries with same key (same UserLogin+PermissionLevel+AccessType+GrantedThrough, different sites) produce 1 row with 3 locations - RPT-04-d: Entries with different keys (different PermissionLevel) remain as separate rows - RPT-04-e: Case-insensitive key — "ALICE@contoso.com" and "alice@contoso.com" merge into same group - RPT-04-f: MakeKey produces pipe-delimited format "userlogin|permissionlevel|accesstype|grantedthrough" (all lowercase strings) - RPT-04-g: 10-row input with 3 duplicate pairs produces exactly 7 consolidated rows - RPT-04-h: LocationCount property equals Locations.Count for a merged entry - RPT-04-i: IsHighPrivilege=true and IsExternalUser=true from first entry are preserved in consolidated result - RPT-04-j: Full solution builds cleanly (verified by build command, not a test method) Create `PermissionConsolidatorTests.cs` in `SharepointToolbox.Tests/Helpers/`. Follow existing test conventions from `PermissionLevelMappingTests.cs`: - Namespace: `SharepointToolbox.Tests.Helpers` - Use `[Fact]` for each test - PascalCase method names with descriptive names Include a private helper factory method to reduce boilerplate: ```csharp private static UserAccessEntry MakeEntry( string userLogin = "alice@contoso.com", string siteUrl = "https://contoso.sharepoint.com/sites/hr", string siteTitle = "HR Site", string objectType = "List", string objectTitle = "Documents", string objectUrl = "https://contoso.sharepoint.com/sites/hr/Documents", string permissionLevel = "Contribute", AccessType accessType = AccessType.Direct, string grantedThrough = "Direct Permissions", string userDisplayName = "Alice Smith", bool isHighPrivilege = false, bool isExternalUser = false) { return new UserAccessEntry( userDisplayName, userLogin, siteUrl, siteTitle, objectType, objectTitle, objectUrl, permissionLevel, accessType, grantedThrough, isHighPrivilege, isExternalUser); } ``` **Test implementations:** **RPT-04-a** `Consolidate_EmptyInput_ReturnsEmptyList`: - `var result = PermissionConsolidator.Consolidate(Array.Empty());` - Assert: `result` is empty. **RPT-04-b** `Consolidate_SingleEntry_ReturnsOneRowWithOneLocation`: - Create 1 entry via `MakeEntry()`. - Assert: result count is 1, result[0].Locations.Count is 1, result[0].UserLogin matches. **RPT-04-c** `Consolidate_ThreeEntriesSameKey_ReturnsOneRowWithThreeLocations`: - Create 3 entries with same UserLogin/PermissionLevel/AccessType/GrantedThrough but different SiteUrl/SiteTitle. - Assert: result count is 1, result[0].Locations.Count is 3. **RPT-04-d** `Consolidate_DifferentKeys_RemainSeparateRows`: - Create 2 entries with same UserLogin but different PermissionLevel ("Contribute" vs "Full Control"). - Assert: result count is 2. **RPT-04-e** `Consolidate_CaseInsensitiveKey_MergesCorrectly`: - Create 2 entries: one with UserLogin "ALICE@CONTOSO.COM" and one with "alice@contoso.com", same other key fields. - Assert: result count is 1, result[0].Locations.Count is 2. **RPT-04-f** `MakeKey_ProducesPipeDelimitedLowercaseFormat`: - Create entry with UserLogin="Alice@Contoso.com", PermissionLevel="Full Control", AccessType=Direct, GrantedThrough="Direct Permissions". - Call `PermissionConsolidator.MakeKey(entry)`. - Assert: result equals "alice@contoso.com|full control|Direct|direct permissions" (note: enum ToString() preserves case). **RPT-04-g** `Consolidate_TenRowsWithThreeDuplicatePairs_ReturnsSevenRows`: - Build exactly 10 entries: - 3 entries for alice@contoso.com / Contribute / Direct / "Direct Permissions" (different sites) -> merges to 1 - 2 entries for bob@contoso.com / Full Control / Group / "SharePoint Group: Owners" (different sites) -> merges to 1 - 2 entries for carol@contoso.com / Read / Inherited / "Inherited Permissions" (different sites) -> merges to 1 - 1 entry for alice@contoso.com / Full Control / Direct / "Direct Permissions" (different key from alice's Contribute) - 1 entry for dave@contoso.com / Contribute / Direct / "Direct Permissions" - 1 entry for eve@contoso.com / Read / Direct / "Direct Permissions" - Assert: result.Count is 7 (3 pairs merged to 1 each = 3, plus 4 unique = 7). **RPT-04-h** `Consolidate_MergedEntry_LocationCountMatchesLocationsCount`: - Create 3 entries with same key. - Assert: result[0].LocationCount == result[0].Locations.Count && result[0].LocationCount == 3. **RPT-04-i** `Consolidate_PreservesIsHighPrivilegeAndIsExternalUser`: - Create 2 entries with same key, first has IsHighPrivilege=true, IsExternalUser=true. - Assert: consolidated result has IsHighPrivilege=true and IsExternalUser=true. Use `using SharepointToolbox.Core.Helpers;` and `using SharepointToolbox.Core.Models;`. cd C:/Users/dev/Documents/projets/Sharepoint && dotnet test SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~PermissionConsolidatorTests" --no-restore -v n 2>&1 | tail -20 All 9 test methods pass (RPT-04-a through RPT-04-i). Tests cover empty input, single entry, merging, separate keys, case insensitivity, MakeKey format, the 10-row scenario, LocationCount, and preserved flags. Task 2: Verify full solution build (existing exports unchanged) Run a full solution build to confirm: 1. The three new files (LocationInfo.cs, ConsolidatedPermissionEntry.cs, PermissionConsolidator.cs) compile. 2. The test file (PermissionConsolidatorTests.cs) compiles. 3. All existing code — especially `UserAccessHtmlExportService`, `HtmlExportService`, and `UserAccessAuditService` — compiles without modification. 4. No existing tests are broken. Run: `dotnet build` (full solution) and then `dotnet test` (all tests). This satisfies success criterion 4: "Existing HTML export services compile and produce identical output when consolidation is not applied (opt-in, defaults off)." Since no existing files were modified and the new code is opt-in only, existing behavior is unchanged by definition. If the build or any existing test fails, investigate and fix — the new files must not introduce any regressions. cd C:/Users/dev/Documents/projets/Sharepoint && dotnet build -v q 2>&1 | tail -5 && dotnet test --no-build -v n 2>&1 | tail -10 Full solution builds with 0 errors. All existing tests plus new PermissionConsolidatorTests pass. No existing files modified. All tests pass including the new PermissionConsolidatorTests: ```bash cd C:/Users/dev/Documents/projets/Sharepoint && dotnet test -v n ``` Specific verification of the 10-row scenario (success criterion 3): ```bash cd C:/Users/dev/Documents/projets/Sharepoint && dotnet test --filter "FullyQualifiedName~TenRowsWithThreeDuplicatePairs" -v n ``` - PermissionConsolidatorTests.cs exists with 9 [Fact] test methods covering RPT-04-a through RPT-04-i - All 9 tests pass - The 10-row input test produces exactly 7 output rows - Full solution build succeeds with 0 errors (RPT-04-j) - All pre-existing tests continue to pass After completion, create `.planning/phases/15-consolidation-data-model/15-02-SUMMARY.md`