docs(15): create consolidation data model phase plans
Two plans for Phase 15: models + consolidator service (wave 1), unit tests + build verification (wave 2). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,10 @@
|
|||||||
2. A `PermissionConsolidator` service accepts a flat list of permission rows and returns a consolidated list where duplicate user+level rows are merged
|
2. A `PermissionConsolidator` service accepts a flat list of permission rows and returns a consolidated list where duplicate user+level rows are merged
|
||||||
3. Consolidation logic has unit test coverage — a known 10-row input with 3 duplicate pairs produces the expected 7-row output
|
3. Consolidation logic has unit test coverage — a known 10-row input with 3 duplicate pairs produces the expected 7-row output
|
||||||
4. Existing HTML export services compile and produce identical output when consolidation is not applied (opt-in, defaults off)
|
4. Existing HTML export services compile and produce identical output when consolidation is not applied (opt-in, defaults off)
|
||||||
**Plans**: TBD
|
**Plans:** 2 plans
|
||||||
|
Plans:
|
||||||
|
- [ ] 15-01-PLAN.md — Models (LocationInfo, ConsolidatedPermissionEntry) + PermissionConsolidator service
|
||||||
|
- [ ] 15-02-PLAN.md — Unit tests (10 test cases) + full solution build verification
|
||||||
|
|
||||||
### Phase 16: Report Consolidation Toggle
|
### Phase 16: Report Consolidation Toggle
|
||||||
**Goal**: Users can choose to merge duplicate permission entries per export through a toggle in the export settings dialog
|
**Goal**: Users can choose to merge duplicate permission entries per export through a toggle in the export settings dialog
|
||||||
@@ -114,7 +117,7 @@
|
|||||||
| 1-5 | v1.0 | 36/36 | Shipped | 2026-04-07 |
|
| 1-5 | v1.0 | 36/36 | Shipped | 2026-04-07 |
|
||||||
| 6-9 | v1.1 | 25/25 | Shipped | 2026-04-08 |
|
| 6-9 | v1.1 | 25/25 | Shipped | 2026-04-08 |
|
||||||
| 10-14 | v2.2 | 14/14 | Shipped | 2026-04-09 |
|
| 10-14 | v2.2 | 14/14 | Shipped | 2026-04-09 |
|
||||||
| 15. Consolidation Data Model | v2.3 | 0/? | Not started | — |
|
| 15. Consolidation Data Model | v2.3 | 0/2 | Planning | — |
|
||||||
| 16. Report Consolidation Toggle | v2.3 | 0/? | Not started | — |
|
| 16. Report Consolidation Toggle | v2.3 | 0/? | Not started | — |
|
||||||
| 17. Group Expansion in HTML Reports | v2.3 | 0/? | Not started | — |
|
| 17. Group Expansion in HTML Reports | v2.3 | 0/? | Not started | — |
|
||||||
| 18. Auto-Take Ownership | v2.3 | 0/? | Not started | — |
|
| 18. Auto-Take Ownership | v2.3 | 0/? | Not started | — |
|
||||||
|
|||||||
239
.planning/phases/15-consolidation-data-model/15-01-PLAN.md
Normal file
239
.planning/phases/15-consolidation-data-model/15-01-PLAN.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
---
|
||||||
|
phase: 15-consolidation-data-model
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- SharepointToolbox/Core/Models/LocationInfo.cs
|
||||||
|
- SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs
|
||||||
|
- SharepointToolbox/Core/Helpers/PermissionConsolidator.cs
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- RPT-04
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "LocationInfo record holds five location fields from UserAccessEntry"
|
||||||
|
- "ConsolidatedPermissionEntry holds key fields plus a list of LocationInfo with LocationCount"
|
||||||
|
- "PermissionConsolidator.Consolidate merges entries with identical key into single rows"
|
||||||
|
- "MakeKey uses pipe-delimited case-insensitive composite of UserLogin+PermissionLevel+AccessType+GrantedThrough"
|
||||||
|
- "Empty input returns empty list"
|
||||||
|
artifacts:
|
||||||
|
- path: "SharepointToolbox/Core/Models/LocationInfo.cs"
|
||||||
|
provides: "Location data record"
|
||||||
|
contains: "public record LocationInfo"
|
||||||
|
- path: "SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs"
|
||||||
|
provides: "Consolidated permission model"
|
||||||
|
contains: "public record ConsolidatedPermissionEntry"
|
||||||
|
- path: "SharepointToolbox/Core/Helpers/PermissionConsolidator.cs"
|
||||||
|
provides: "Consolidation logic"
|
||||||
|
exports: ["Consolidate", "MakeKey"]
|
||||||
|
key_links:
|
||||||
|
- from: "SharepointToolbox/Core/Helpers/PermissionConsolidator.cs"
|
||||||
|
to: "SharepointToolbox/Core/Models/UserAccessEntry.cs"
|
||||||
|
via: "accepts IReadOnlyList<UserAccessEntry>"
|
||||||
|
pattern: "IReadOnlyList<UserAccessEntry>"
|
||||||
|
- from: "SharepointToolbox/Core/Helpers/PermissionConsolidator.cs"
|
||||||
|
to: "SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs"
|
||||||
|
via: "returns IReadOnlyList<ConsolidatedPermissionEntry>"
|
||||||
|
pattern: "IReadOnlyList<ConsolidatedPermissionEntry>"
|
||||||
|
- from: "SharepointToolbox/Core/Helpers/PermissionConsolidator.cs"
|
||||||
|
to: "SharepointToolbox/Core/Models/LocationInfo.cs"
|
||||||
|
via: "constructs LocationInfo from UserAccessEntry fields"
|
||||||
|
pattern: "new LocationInfo"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create the consolidation data model and merge service for permission report consolidation.
|
||||||
|
|
||||||
|
Purpose: Establish the data shape (LocationInfo, ConsolidatedPermissionEntry) and pure-function merge logic (PermissionConsolidator) so that Phase 16 can wire them into the export pipeline. Zero API calls, zero UI — just models and a static helper.
|
||||||
|
|
||||||
|
Output: Three production files — two model records and one static consolidation service.
|
||||||
|
</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/STATE.md
|
||||||
|
@.planning/phases/15-consolidation-data-model/15-CONTEXT.md
|
||||||
|
@.planning/phases/15-consolidation-data-model/15-RESEARCH.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Source model the consolidator consumes. From SharepointToolbox/Core/Models/UserAccessEntry.cs -->
|
||||||
|
```csharp
|
||||||
|
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
|
||||||
|
);
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create LocationInfo and ConsolidatedPermissionEntry model records</name>
|
||||||
|
<files>SharepointToolbox/Core/Models/LocationInfo.cs, SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs</files>
|
||||||
|
<action>
|
||||||
|
Create two new C# positional record files in `Core/Models/`.
|
||||||
|
|
||||||
|
**LocationInfo.cs** — namespace `SharepointToolbox.Core.Models`:
|
||||||
|
```csharp
|
||||||
|
public record LocationInfo(
|
||||||
|
string SiteUrl,
|
||||||
|
string SiteTitle,
|
||||||
|
string ObjectTitle,
|
||||||
|
string ObjectUrl,
|
||||||
|
string ObjectType
|
||||||
|
);
|
||||||
|
```
|
||||||
|
Lightweight record holding the five location-related fields extracted from UserAccessEntry when rows are merged.
|
||||||
|
|
||||||
|
**ConsolidatedPermissionEntry.cs** — namespace `SharepointToolbox.Core.Models`:
|
||||||
|
```csharp
|
||||||
|
public record ConsolidatedPermissionEntry(
|
||||||
|
string UserDisplayName,
|
||||||
|
string UserLogin,
|
||||||
|
string PermissionLevel,
|
||||||
|
AccessType AccessType,
|
||||||
|
string GrantedThrough,
|
||||||
|
bool IsHighPrivilege,
|
||||||
|
bool IsExternalUser,
|
||||||
|
IReadOnlyList<LocationInfo> Locations
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public int LocationCount => Locations.Count;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Holds the four key fields (UserLogin, PermissionLevel, AccessType, GrantedThrough) plus carried-forward fields (UserDisplayName, IsHighPrivilege, IsExternalUser).
|
||||||
|
- `Locations` is an `IReadOnlyList<LocationInfo>` containing all merged locations.
|
||||||
|
- `LocationCount` is a computed convenience property.
|
||||||
|
- Do NOT add any methods, constructors, or logic beyond the record definition and LocationCount property.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd C:/Users/dev/Documents/projets/Sharepoint && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -v q 2>&1 | tail -5</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Both record files exist, compile without errors, and are in the SharepointToolbox.Core.Models namespace. ConsolidatedPermissionEntry.LocationCount returns Locations.Count.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create PermissionConsolidator static helper</name>
|
||||||
|
<files>SharepointToolbox/Core/Helpers/PermissionConsolidator.cs</files>
|
||||||
|
<action>
|
||||||
|
Create `PermissionConsolidator.cs` in `Core/Helpers/` — namespace `SharepointToolbox.Core.Helpers`.
|
||||||
|
|
||||||
|
Follow the existing `DuplicatesService.MakeKey()` pattern for composite key generation.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using SharepointToolbox.Core.Models;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.Core.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Merges a flat list of UserAccessEntry rows into consolidated entries
|
||||||
|
/// where rows with identical (UserLogin, PermissionLevel, AccessType, GrantedThrough)
|
||||||
|
/// are grouped into a single row with multiple locations.
|
||||||
|
/// </summary>
|
||||||
|
public static class PermissionConsolidator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a pipe-delimited, case-insensitive composite key from the four key fields.
|
||||||
|
/// </summary>
|
||||||
|
internal static string MakeKey(UserAccessEntry entry)
|
||||||
|
{
|
||||||
|
return string.Join("|",
|
||||||
|
entry.UserLogin.ToLowerInvariant(),
|
||||||
|
entry.PermissionLevel.ToLowerInvariant(),
|
||||||
|
entry.AccessType.ToString(),
|
||||||
|
entry.GrantedThrough.ToLowerInvariant());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Groups entries by composite key and returns consolidated rows.
|
||||||
|
/// Each group's first entry provides UserDisplayName, IsHighPrivilege, IsExternalUser.
|
||||||
|
/// All entries in a group contribute a LocationInfo to the Locations list.
|
||||||
|
/// Results are ordered by UserLogin then PermissionLevel.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<ConsolidatedPermissionEntry> Consolidate(
|
||||||
|
IReadOnlyList<UserAccessEntry> entries)
|
||||||
|
{
|
||||||
|
if (entries.Count == 0)
|
||||||
|
return Array.Empty<ConsolidatedPermissionEntry>();
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.GroupBy(e => MakeKey(e))
|
||||||
|
.Select(g =>
|
||||||
|
{
|
||||||
|
var first = g.First();
|
||||||
|
var locations = g.Select(e => new LocationInfo(
|
||||||
|
e.SiteUrl, e.SiteTitle, e.ObjectTitle, e.ObjectUrl, e.ObjectType
|
||||||
|
)).ToList();
|
||||||
|
|
||||||
|
return new ConsolidatedPermissionEntry(
|
||||||
|
first.UserDisplayName,
|
||||||
|
first.UserLogin,
|
||||||
|
first.PermissionLevel,
|
||||||
|
first.AccessType,
|
||||||
|
first.GrantedThrough,
|
||||||
|
first.IsHighPrivilege,
|
||||||
|
first.IsExternalUser,
|
||||||
|
locations);
|
||||||
|
})
|
||||||
|
.OrderBy(c => c.UserLogin)
|
||||||
|
.ThenBy(c => c.PermissionLevel)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key implementation details:
|
||||||
|
- `MakeKey` is `internal` so tests can access it via `[InternalsVisibleTo]` or by testing through `Consolidate`.
|
||||||
|
- Use `.ToLowerInvariant()` on UserLogin, PermissionLevel, GrantedThrough (string key fields). AccessType is an enum — use `.ToString()` (case-stable).
|
||||||
|
- Empty input short-circuits to `Array.Empty<>()`.
|
||||||
|
- LINQ GroupBy + Select pattern — no mutable dictionaries.
|
||||||
|
- OrderBy UserLogin then PermissionLevel for deterministic output.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd C:/Users/dev/Documents/projets/Sharepoint && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -v q 2>&1 | tail -5</automated>
|
||||||
|
</verify>
|
||||||
|
<done>PermissionConsolidator.cs compiles. Consolidate method accepts IReadOnlyList of UserAccessEntry, returns IReadOnlyList of ConsolidatedPermissionEntry. MakeKey produces pipe-delimited lowercase composite key.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Full solution build passes:
|
||||||
|
```bash
|
||||||
|
cd C:/Users/dev/Documents/projets/Sharepoint && dotnet build --no-restore -v q
|
||||||
|
```
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- LocationInfo.cs and ConsolidatedPermissionEntry.cs exist in Core/Models/ with correct record signatures
|
||||||
|
- PermissionConsolidator.cs exists in Core/Helpers/ with Consolidate and MakeKey methods
|
||||||
|
- All three files compile as part of the SharepointToolbox project
|
||||||
|
- No changes to any existing files
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/15-consolidation-data-model/15-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
250
.planning/phases/15-consolidation-data-model/15-02-PLAN.md
Normal file
250
.planning/phases/15-consolidation-data-model/15-02-PLAN.md
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
---
|
||||||
|
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"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
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.
|
||||||
|
</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/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
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- From Plan 01 outputs -->
|
||||||
|
```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<LocationInfo> 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<ConsolidatedPermissionEntry> Consolidate(IReadOnlyList<UserAccessEntry> 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
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- InternalsVisibleTo already configured in SharepointToolbox/AssemblyInfo.cs -->
|
||||||
|
<!-- Test conventions: xUnit, [Fact]/[Theory], snake_case or PascalCase_Condition_Expected naming -->
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Create PermissionConsolidatorTests with all 10 test cases</name>
|
||||||
|
<files>SharepointToolbox.Tests/Helpers/PermissionConsolidatorTests.cs</files>
|
||||||
|
<behavior>
|
||||||
|
- 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)
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
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<UserAccessEntry>());`
|
||||||
|
- 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;`.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>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</automated>
|
||||||
|
</verify>
|
||||||
|
<done>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.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Verify full solution build (existing exports unchanged)</name>
|
||||||
|
<files></files>
|
||||||
|
<action>
|
||||||
|
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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>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</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Full solution builds with 0 errors. All existing tests plus new PermissionConsolidatorTests pass. No existing files modified.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
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
|
||||||
|
```
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- 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
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/15-consolidation-data-model/15-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user