9.8 KiB
9.8 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 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 17-group-expansion-html-reports | 01 | execute | 1 |
|
true |
|
|
Purpose: This service is the data provider for Phase 17 — it pre-resolves group members before HTML export so the export service remains pure and synchronous.
Output: ResolvedMember model, ISharePointGroupResolver interface, SharePointGroupResolver implementation, DI registration, unit tests.
<execution_context> @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/17-group-expansion-html-reports/17-RESEARCH.mdFrom SharepointToolbox/Core/Models/PermissionEntry.cs:
public record PermissionEntry(
string ObjectType,
string Title,
string Url,
bool HasUniquePermissions,
string Users,
string UserLogins,
string PermissionLevels,
string GrantedThrough,
string PrincipalType // "SharePointGroup" | "User" | "External User"
);
From SharepointToolbox/Infrastructure/Auth/GraphClientFactory.cs:
public class GraphClientFactory
{
public async Task<GraphServiceClient> CreateClientAsync(string clientId, CancellationToken ct)
}
From SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs:
public static class ExecuteQueryRetryHelper
{
public static async Task ExecuteQueryRetryAsync(
ClientContext ctx,
IProgress<OperationProgress>? progress = null,
CancellationToken ct = default)
}
From SharepointToolbox/App.xaml.cs (DI registration pattern):
// Phase 4: Bulk Members
services.AddTransient<IBulkMemberService, BulkMemberService>();
// Phase 7: User Access Audit
services.AddTransient<IGraphUserSearchService, GraphUserSearchService>();
2. Create `Services/ISharePointGroupResolver.cs`:
```csharp
public interface ISharePointGroupResolver
{
Task<IReadOnlyDictionary<string, IReadOnlyList<ResolvedMember>>> ResolveGroupsAsync(
ClientContext ctx, string clientId,
IReadOnlyList<string> groupNames, CancellationToken ct);
}
```
3. Create `Services/SharePointGroupResolver.cs` implementing `ISharePointGroupResolver`:
- Constructor takes `GraphClientFactory` (same pattern as `BulkMemberService`)
- `ResolveGroupsAsync` iterates group names (`.Distinct(StringComparer.OrdinalIgnoreCase)`)
- Per group: CSOM `ctx.Web.SiteGroups.GetByName(name).Users` with `ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct)`
- Per user: check `IsAadGroup(loginName)` — if true, extract GUID via `ExtractAadGroupId` and call `ResolveAadGroupAsync` via Graph
- `ResolveAadGroupAsync`: `graphClient.Groups[aadGroupId].TransitiveMembers.GraphUser.GetAsync()` with `PageIterator` for pagination — same pattern as `GraphUserDirectoryService`
- De-duplicate members by Login (OrdinalIgnoreCase) using `.DistinctBy(m => m.Login, StringComparer.OrdinalIgnoreCase)`
- Entire per-group block wrapped in try/catch — on any exception, log warning via `Serilog.Log.Warning` and set `result[groupName] = Array.Empty<ResolvedMember>()`
- Result dictionary created with `StringComparer.OrdinalIgnoreCase`
- Make `IsAadGroup`, `ExtractAadGroupId`, and `StripClaims` internal static (with `InternalsVisibleTo` already set for test project) so they are testable
- `IsAadGroup` pattern: `login.StartsWith("c:0t.c|", StringComparison.OrdinalIgnoreCase)`
- `ExtractAadGroupId`: `login[(login.LastIndexOf('|') + 1)..]`
- `StripClaims`: `login[(login.LastIndexOf('|') + 1)..]` (same substring after last pipe)
4. Create `SharePointGroupResolverTests.cs`:
- Test `IsAadGroup` with true/false cases (see behavior above)
- Test `ExtractAadGroupId` extraction
- Test `StripClaims` extraction
- Test `ResolveGroupsAsync` with empty list returns empty dict (needs mock ClientContext — use `[Fact(Skip="Requires CSOM ClientContext mock")]` if CSOM types cannot be easily mocked; alternatively test the static helpers only and add a skip-marked integration test)
- Test case-insensitive dict: create resolver, call with known group, verify lookup with different casing works. If full resolution cannot be unit tested without live CSOM, mark as `[Fact(Skip="Requires live SP tenant")]` and focus unit tests on the three static helpers
dotnet test --filter "FullyQualifiedName~SharePointGroupResolverTests" --no-build
ResolvedMember record exists, ISharePointGroupResolver interface defined, SharePointGroupResolver compiles with CSOM + Graph resolution, static helpers (IsAadGroup, ExtractAadGroupId, StripClaims) have green unit tests
Task 2: DI registration in App.xaml.cs
SharepointToolbox/App.xaml.cs
Add DI registration for `ISharePointGroupResolver` in `App.xaml.cs` after the Phase 4 Bulk Members block (or near other service registrations):
```csharp
// Phase 17: Group Expansion
services.AddTransient();
```
Add the `using SharepointToolbox.Services;` if not already present (it should be since `IPermissionsService` is already registered from the same namespace).
dotnet build --no-restore 2>&1 | tail -5
ISharePointGroupResolver registered in DI container, solution builds with 0 errors
- `dotnet build` — 0 errors, 0 warnings
- `dotnet test --filter "FullyQualifiedName~SharePointGroupResolverTests"` — all tests pass
- `dotnet test` — full suite green (no regressions)
<success_criteria>
- ResolvedMember record exists at Core/Models/ResolvedMember.cs
- ISharePointGroupResolver interface defines ResolveGroupsAsync contract
- SharePointGroupResolver implements CSOM group user loading + Graph transitive resolution
- Static helpers (IsAadGroup, ExtractAadGroupId, StripClaims) have passing unit tests
- DI registration wired in App.xaml.cs
- Full test suite green </success_criteria>