168 lines
6.4 KiB
C#
168 lines
6.4 KiB
C#
using SharepointToolbox.Services;
|
|
|
|
namespace SharepointToolbox.Tests.Services;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="SharePointGroupResolver"/> (Phase 17 Plan 01).
|
|
///
|
|
/// Testing strategy:
|
|
/// SharePointGroupResolver wraps CSOM (ClientContext) and Microsoft Graph SDK.
|
|
/// Both require live infrastructure that cannot be mocked without heavy ceremony.
|
|
///
|
|
/// We test what IS unit-testable without live infrastructure:
|
|
/// 1. IsAadGroup — static helper: login prefix pattern detection
|
|
/// 2. ExtractAadGroupId — static helper: GUID extraction from AAD group login
|
|
/// 3. StripClaims — static helper: UPN extraction after last pipe
|
|
/// 4. ResolveGroupsAsync with empty list — returns empty dict (no CSOM calls made)
|
|
///
|
|
/// Integration tests requiring live tenant / CSOM context are skip-marked.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public class SharePointGroupResolverTests
|
|
{
|
|
// ── IsAadGroup ─────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void IsAadGroup_AadGroupLogin_ReturnsTrue()
|
|
{
|
|
var login = "c:0t.c|tenant|aaaabbbb-cccc-dddd-eeee-ffffgggghhhh";
|
|
Assert.True(SharePointGroupResolver.IsAadGroup(login));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsAadGroup_RegularUserLogin_ReturnsFalse()
|
|
{
|
|
var login = "i:0#.f|membership|user@contoso.com";
|
|
Assert.False(SharePointGroupResolver.IsAadGroup(login));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsAadGroup_SecurityGroupLogin_ReturnsFalse()
|
|
{
|
|
var login = "c:0(.s|true";
|
|
Assert.False(SharePointGroupResolver.IsAadGroup(login));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsAadGroup_EmptyString_ReturnsFalse()
|
|
{
|
|
Assert.False(SharePointGroupResolver.IsAadGroup(string.Empty));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsAadGroup_CaseInsensitive_ReturnsTrue()
|
|
{
|
|
// Prefix check should be case-insensitive per OrdinalIgnoreCase
|
|
var login = "C:0T.C|TENANT|aaaabbbb-cccc-dddd-eeee-ffffgggghhhh";
|
|
Assert.True(SharePointGroupResolver.IsAadGroup(login));
|
|
}
|
|
|
|
// ── ExtractAadGroupId ──────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ExtractAadGroupId_ValidAadLogin_ExtractsGuid()
|
|
{
|
|
var login = "c:0t.c|tenant|aaaabbbb-cccc-dddd-eeee-ffffgggghhhh";
|
|
var result = SharePointGroupResolver.ExtractAadGroupId(login);
|
|
Assert.Equal("aaaabbbb-cccc-dddd-eeee-ffffgggghhhh", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExtractAadGroupId_SingleSegment_ReturnsFullString()
|
|
{
|
|
// Edge: no pipe — LastIndexOf returns -1 so [(-1+1)..] = [0..] = whole string
|
|
var login = "nopipe";
|
|
var result = SharePointGroupResolver.ExtractAadGroupId(login);
|
|
Assert.Equal("nopipe", result);
|
|
}
|
|
|
|
// ── StripClaims ────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void StripClaims_MembershipLogin_ReturnsUpn()
|
|
{
|
|
var login = "i:0#.f|membership|user@contoso.com";
|
|
var result = SharePointGroupResolver.StripClaims(login);
|
|
Assert.Equal("user@contoso.com", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void StripClaims_NoClaimsPrefix_ReturnsFullString()
|
|
{
|
|
var login = "user@contoso.com";
|
|
var result = SharePointGroupResolver.StripClaims(login);
|
|
Assert.Equal("user@contoso.com", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void StripClaims_MultiPipeLogin_ReturnsAfterLastPipe()
|
|
{
|
|
var login = "c:0t.c|tenant|some-guid-here";
|
|
var result = SharePointGroupResolver.StripClaims(login);
|
|
Assert.Equal("some-guid-here", result);
|
|
}
|
|
|
|
// ── ResolveGroupsAsync — empty input ──────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task ResolveGroupsAsync_EmptyGroupNames_ReturnsEmptyDict()
|
|
{
|
|
// Arrange: create resolver without real dependencies — empty list triggers early return
|
|
// No CSOM ClientContext or GraphClientFactory is called for empty input
|
|
// We pass null! for the factory since it must not be invoked for an empty list
|
|
var resolver = new SharePointGroupResolver(null!);
|
|
|
|
// Act
|
|
var result = await resolver.ResolveGroupsAsync(
|
|
ctx: null!,
|
|
clientId: "ignored",
|
|
groupNames: Array.Empty<string>(),
|
|
ct: CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResolveGroupsAsync_EmptyGroupNames_DictUsesOrdinalIgnoreCase()
|
|
{
|
|
// Arrange
|
|
var resolver = new SharePointGroupResolver(null!);
|
|
|
|
// Act
|
|
var result = await resolver.ResolveGroupsAsync(
|
|
ctx: null!,
|
|
clientId: "ignored",
|
|
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.True(mutable.ContainsKey("site members"),
|
|
"Result dictionary comparer must be OrdinalIgnoreCase");
|
|
}
|
|
|
|
// ── Integration tests (live SP tenant required) ────────────────────────────
|
|
|
|
[Fact(Skip = "Requires live SP tenant — run manually against a real ClientContext")]
|
|
public async Task ResolveGroupsAsync_KnownGroup_ReturnsMembers()
|
|
{
|
|
// Integration test: create a real ClientContext, call with a known group name,
|
|
// verify the returned list contains at least one ResolvedMember.
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
[Fact(Skip = "Requires live SP tenant — verify case-insensitive lookup with real data")]
|
|
public async Task ResolveGroupsAsync_LookupDifferentCasing_FindsGroup()
|
|
{
|
|
// Integration test: resolver stores "Site Members" — lookup "site members" should succeed.
|
|
await Task.CompletedTask;
|
|
}
|
|
}
|