Files
Sharepoint-Toolbox/SharepointToolbox.Tests/Services/SharePointGroupResolverTests.cs

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;
}
}