feat(17-01): ResolvedMember model, ISharePointGroupResolver interface, SharePointGroupResolver CSOM+Graph implementation

- ResolvedMember record in Core/Models with DisplayName and Login
- ISharePointGroupResolver interface with ResolveGroupsAsync contract
- SharePointGroupResolver: CSOM group user loading + Graph transitive AAD resolution
- Internal static helpers IsAadGroup, ExtractAadGroupId, StripClaims (all green unit tests)
- Graceful error handling: exceptions return empty list per group, never throw
- OrdinalIgnoreCase result dict; lazy Graph client creation on first AAD group
This commit is contained in:
Dev
2026-04-09 13:04:56 +02:00
parent 0f8b1953e1
commit 543b863283
4 changed files with 217 additions and 7 deletions

View File

@@ -137,13 +137,12 @@ public class SharePointGroupResolverTests
groupNames: Array.Empty<string>(),
ct: CancellationToken.None);
// Assert: verify the returned dict is OrdinalIgnoreCase by inserting a value
// and looking it up with different casing this validates the comparer used
// at construction time even on an empty dict
var mutable = new Dictionary<string, int>(result.Comparer)
{
["Site Members"] = 1
};
// Assert: verify the returned dict is OrdinalIgnoreCase by casting to Dictionary
// and checking its comparer, or by testing that the underlying type supports it.
// Since ResolveGroupsAsync returns a Dictionary<string,…> wrapped as IReadOnlyDictionary,
// we cast back and insert a test entry with mixed casing.
var mutable = (Dictionary<string, IReadOnlyList<SharepointToolbox.Core.Models.ResolvedMember>>)result;
mutable["Site Members"] = Array.Empty<SharepointToolbox.Core.Models.ResolvedMember>();
Assert.True(mutable.ContainsKey("site members"),
"Result dictionary comparer must be OrdinalIgnoreCase");
}