diff --git a/SharepointToolbox.Tests/Helpers/PermissionConsolidatorTests.cs b/SharepointToolbox.Tests/Helpers/PermissionConsolidatorTests.cs
new file mode 100644
index 0000000..8da120b
--- /dev/null
+++ b/SharepointToolbox.Tests/Helpers/PermissionConsolidatorTests.cs
@@ -0,0 +1,256 @@
+using SharepointToolbox.Core.Helpers;
+using SharepointToolbox.Core.Models;
+
+namespace SharepointToolbox.Tests.Helpers;
+
+///
+/// Unit tests for PermissionConsolidator static helper.
+/// RPT-04: Validates consolidation logic for empty input, single entry, merging,
+/// case-insensitivity, MakeKey format, the 10-row/7-row scenario, LocationCount,
+/// and preservation of IsHighPrivilege / IsExternalUser flags.
+///
+public class PermissionConsolidatorTests
+{
+ // ---------------------------------------------------------------------------
+ // Helper factory — reduces boilerplate across all test methods
+ // ---------------------------------------------------------------------------
+
+ 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);
+ }
+
+ // ---------------------------------------------------------------------------
+ // RPT-04-a: Empty input returns empty list
+ // ---------------------------------------------------------------------------
+
+ [Fact]
+ public void Consolidate_EmptyInput_ReturnsEmptyList()
+ {
+ var result = PermissionConsolidator.Consolidate(Array.Empty());
+
+ Assert.Empty(result);
+ }
+
+ // ---------------------------------------------------------------------------
+ // RPT-04-b: Single entry produces 1 consolidated row with 1 location
+ // ---------------------------------------------------------------------------
+
+ [Fact]
+ public void Consolidate_SingleEntry_ReturnsOneRowWithOneLocation()
+ {
+ var entry = MakeEntry();
+
+ var result = PermissionConsolidator.Consolidate(new[] { entry });
+
+ var row = Assert.Single(result);
+ Assert.Single(row.Locations);
+ Assert.Equal("alice@contoso.com", row.UserLogin);
+ }
+
+ // ---------------------------------------------------------------------------
+ // RPT-04-c: 3 entries with same key (different sites) merge to 1 row with 3 locations
+ // ---------------------------------------------------------------------------
+
+ [Fact]
+ public void Consolidate_ThreeEntriesSameKey_ReturnsOneRowWithThreeLocations()
+ {
+ var entries = new[]
+ {
+ MakeEntry(siteUrl: "https://contoso.sharepoint.com/sites/hr", siteTitle: "HR Site"),
+ MakeEntry(siteUrl: "https://contoso.sharepoint.com/sites/fin", siteTitle: "Finance Site"),
+ MakeEntry(siteUrl: "https://contoso.sharepoint.com/sites/mkt", siteTitle: "Marketing Site"),
+ };
+
+ var result = PermissionConsolidator.Consolidate(entries);
+
+ Assert.Single(result);
+ Assert.Equal(3, result[0].Locations.Count);
+ }
+
+ // ---------------------------------------------------------------------------
+ // RPT-04-d: Entries with different keys remain as separate rows
+ // ---------------------------------------------------------------------------
+
+ [Fact]
+ public void Consolidate_DifferentKeys_RemainSeparateRows()
+ {
+ var entries = new[]
+ {
+ MakeEntry(permissionLevel: "Contribute"),
+ MakeEntry(permissionLevel: "Full Control"),
+ };
+
+ var result = PermissionConsolidator.Consolidate(entries);
+
+ Assert.Equal(2, result.Count);
+ }
+
+ // ---------------------------------------------------------------------------
+ // RPT-04-e: Case-insensitive key — "ALICE@CONTOSO.COM" and "alice@contoso.com" merge
+ // ---------------------------------------------------------------------------
+
+ [Fact]
+ public void Consolidate_CaseInsensitiveKey_MergesCorrectly()
+ {
+ var entries = new[]
+ {
+ MakeEntry(userLogin: "ALICE@CONTOSO.COM", siteUrl: "https://contoso.sharepoint.com/sites/hr"),
+ MakeEntry(userLogin: "alice@contoso.com", siteUrl: "https://contoso.sharepoint.com/sites/fin"),
+ };
+
+ var result = PermissionConsolidator.Consolidate(entries);
+
+ Assert.Single(result);
+ Assert.Equal(2, result[0].Locations.Count);
+ }
+
+ // ---------------------------------------------------------------------------
+ // RPT-04-f: MakeKey produces pipe-delimited lowercase format
+ // ---------------------------------------------------------------------------
+
+ [Fact]
+ public void MakeKey_ProducesPipeDelimitedLowercaseFormat()
+ {
+ var entry = MakeEntry(
+ userLogin: "Alice@Contoso.com",
+ permissionLevel: "Full Control",
+ accessType: AccessType.Direct,
+ grantedThrough: "Direct Permissions");
+
+ var key = PermissionConsolidator.MakeKey(entry);
+
+ // AccessType.ToString() preserves casing ("Direct"); all string fields are lowercased
+ Assert.Equal("alice@contoso.com|full control|Direct|direct permissions", key);
+ }
+
+ // ---------------------------------------------------------------------------
+ // RPT-04-g: 10-row input with 3 duplicate pairs produces 7 consolidated rows
+ // ---------------------------------------------------------------------------
+
+ [Fact]
+ public void Consolidate_TenRowsWithThreeDuplicatePairs_ReturnsSevenRows()
+ {
+ var entries = new[]
+ {
+ // alice / Contribute / Direct — 3 entries -> merges to 1
+ MakeEntry(userLogin: "alice@contoso.com", permissionLevel: "Contribute",
+ accessType: AccessType.Direct, grantedThrough: "Direct Permissions",
+ siteUrl: "https://contoso.sharepoint.com/sites/hr", siteTitle: "HR"),
+ MakeEntry(userLogin: "alice@contoso.com", permissionLevel: "Contribute",
+ accessType: AccessType.Direct, grantedThrough: "Direct Permissions",
+ siteUrl: "https://contoso.sharepoint.com/sites/fin", siteTitle: "Finance"),
+ MakeEntry(userLogin: "alice@contoso.com", permissionLevel: "Contribute",
+ accessType: AccessType.Direct, grantedThrough: "Direct Permissions",
+ siteUrl: "https://contoso.sharepoint.com/sites/mkt", siteTitle: "Marketing"),
+
+ // bob / Full Control / Group — 2 entries -> merges to 1
+ MakeEntry(userLogin: "bob@contoso.com", userDisplayName: "Bob Jones",
+ permissionLevel: "Full Control", accessType: AccessType.Group,
+ grantedThrough: "SharePoint Group: Owners",
+ siteUrl: "https://contoso.sharepoint.com/sites/hr", siteTitle: "HR"),
+ MakeEntry(userLogin: "bob@contoso.com", userDisplayName: "Bob Jones",
+ permissionLevel: "Full Control", accessType: AccessType.Group,
+ grantedThrough: "SharePoint Group: Owners",
+ siteUrl: "https://contoso.sharepoint.com/sites/fin", siteTitle: "Finance"),
+
+ // carol / Read / Inherited — 2 entries -> merges to 1
+ MakeEntry(userLogin: "carol@contoso.com", userDisplayName: "Carol White",
+ permissionLevel: "Read", accessType: AccessType.Inherited,
+ grantedThrough: "Inherited Permissions",
+ siteUrl: "https://contoso.sharepoint.com/sites/hr", siteTitle: "HR"),
+ MakeEntry(userLogin: "carol@contoso.com", userDisplayName: "Carol White",
+ permissionLevel: "Read", accessType: AccessType.Inherited,
+ grantedThrough: "Inherited Permissions",
+ siteUrl: "https://contoso.sharepoint.com/sites/fin", siteTitle: "Finance"),
+
+ // alice / Full Control / Direct — different key from alice's Contribute -> unique row
+ MakeEntry(userLogin: "alice@contoso.com", permissionLevel: "Full Control",
+ accessType: AccessType.Direct, grantedThrough: "Direct Permissions",
+ siteUrl: "https://contoso.sharepoint.com/sites/hr", siteTitle: "HR"),
+
+ // dave — unique
+ MakeEntry(userLogin: "dave@contoso.com", userDisplayName: "Dave Brown",
+ permissionLevel: "Contribute", accessType: AccessType.Direct,
+ grantedThrough: "Direct Permissions",
+ siteUrl: "https://contoso.sharepoint.com/sites/hr", siteTitle: "HR"),
+
+ // eve — unique
+ MakeEntry(userLogin: "eve@contoso.com", userDisplayName: "Eve Green",
+ permissionLevel: "Read", accessType: AccessType.Direct,
+ grantedThrough: "Direct Permissions",
+ siteUrl: "https://contoso.sharepoint.com/sites/hr", siteTitle: "HR"),
+
+ // frank — unique (4th unique row)
+ MakeEntry(userLogin: "frank@contoso.com", userDisplayName: "Frank Black",
+ permissionLevel: "Contribute", accessType: AccessType.Group,
+ grantedThrough: "SharePoint Group: Members",
+ siteUrl: "https://contoso.sharepoint.com/sites/hr", siteTitle: "HR"),
+ };
+
+ var result = PermissionConsolidator.Consolidate(entries);
+
+ // 3 merged groups (alice-Contribute 3->1, bob 2->1, carol 2->1) + 4 unique rows
+ // (alice-FullControl, dave, eve, frank) = 7 total
+ Assert.Equal(7, result.Count);
+ }
+
+ // ---------------------------------------------------------------------------
+ // RPT-04-h: LocationCount property equals Locations.Count for a merged entry
+ // ---------------------------------------------------------------------------
+
+ [Fact]
+ public void Consolidate_MergedEntry_LocationCountMatchesLocationsCount()
+ {
+ var entries = new[]
+ {
+ MakeEntry(siteUrl: "https://contoso.sharepoint.com/sites/hr", siteTitle: "HR"),
+ MakeEntry(siteUrl: "https://contoso.sharepoint.com/sites/fin", siteTitle: "Finance"),
+ MakeEntry(siteUrl: "https://contoso.sharepoint.com/sites/mkt", siteTitle: "Marketing"),
+ };
+
+ var result = PermissionConsolidator.Consolidate(entries);
+
+ Assert.Single(result);
+ Assert.Equal(result[0].Locations.Count, result[0].LocationCount);
+ Assert.Equal(3, result[0].LocationCount);
+ }
+
+ // ---------------------------------------------------------------------------
+ // RPT-04-i: IsHighPrivilege and IsExternalUser from first entry are preserved
+ // ---------------------------------------------------------------------------
+
+ [Fact]
+ public void Consolidate_PreservesIsHighPrivilegeAndIsExternalUser()
+ {
+ var entries = new[]
+ {
+ MakeEntry(isHighPrivilege: true, isExternalUser: true,
+ siteUrl: "https://contoso.sharepoint.com/sites/hr"),
+ MakeEntry(isHighPrivilege: false, isExternalUser: false,
+ siteUrl: "https://contoso.sharepoint.com/sites/fin"),
+ };
+
+ var result = PermissionConsolidator.Consolidate(entries);
+
+ Assert.Single(result);
+ Assert.True(result[0].IsHighPrivilege);
+ Assert.True(result[0].IsExternalUser);
+ }
+}