diff --git a/SharepointToolbox.Tests/Services/PermissionEntryClassificationTests.cs b/SharepointToolbox.Tests/Services/PermissionEntryClassificationTests.cs new file mode 100644 index 0000000..8cd3e4f --- /dev/null +++ b/SharepointToolbox.Tests/Services/PermissionEntryClassificationTests.cs @@ -0,0 +1,63 @@ +using SharepointToolbox.Core.Helpers; + +namespace SharepointToolbox.Tests.Services; + +/// +/// Tests for PERM-03: external user detection and permission-level filtering. +/// Pure static logic — runs immediately without stubs. +/// +public class PermissionEntryClassificationTests +{ + // ── IsExternalUser ───────────────────────────────────────────────────────── + + [Fact] + public void IsExternalUser_WithExtHashInLoginName_ReturnsTrue() + { + // B2B guest login names contain the literal "#EXT#" fragment + Assert.True(PermissionEntryHelper.IsExternalUser("ext_user_domain.com#EXT#@contoso.onmicrosoft.com")); + } + + [Fact] + public void IsExternalUser_WithNormalLoginName_ReturnsFalse() + { + Assert.False(PermissionEntryHelper.IsExternalUser("i:0#.f|membership|alice@contoso.com")); + } + + // ── FilterPermissionLevels ───────────────────────────────────────────────── + + [Fact] + public void PermissionEntry_FiltersOutLimitedAccess_WhenOnlyPermissionIsLimitedAccess() + { + // A principal whose sole permission level is "Limited Access" should produce + // an empty list after filtering — used to decide whether to include the entry. + var result = PermissionEntryHelper.FilterPermissionLevels(new[] { "Limited Access" }); + Assert.Empty(result); + } + + [Fact] + public void FilterPermissionLevels_RetainsOtherLevels_WhenMixedWithLimitedAccess() + { + var result = PermissionEntryHelper.FilterPermissionLevels(new[] { "Limited Access", "Contribute" }); + Assert.Equal(new[] { "Contribute" }, result); + } + + // ── IsSharingLinksGroup ──────────────────────────────────────────────────── + + [Fact] + public void IsSharingLinksGroup_WithSharingLinksPrefix_ReturnsTrue() + { + Assert.True(PermissionEntryHelper.IsSharingLinksGroup("SharingLinks.abc123.Edit")); + } + + [Fact] + public void IsSharingLinksGroup_WithLimitedAccessSystemGroup_ReturnsTrue() + { + Assert.True(PermissionEntryHelper.IsSharingLinksGroup("Limited Access System Group")); + } + + [Fact] + public void IsSharingLinksGroup_WithNormalGroup_ReturnsFalse() + { + Assert.False(PermissionEntryHelper.IsSharingLinksGroup("Owners")); + } +} diff --git a/SharepointToolbox.Tests/Services/PermissionsServiceTests.cs b/SharepointToolbox.Tests/Services/PermissionsServiceTests.cs new file mode 100644 index 0000000..390233e --- /dev/null +++ b/SharepointToolbox.Tests/Services/PermissionsServiceTests.cs @@ -0,0 +1,31 @@ +using SharepointToolbox.Core.Models; +using SharepointToolbox.Services; + +namespace SharepointToolbox.Tests.Services; + +/// +/// Test stubs for PERM-01 and PERM-04. +/// These tests are skipped until IPermissionsService is implemented in Plan 02. +/// +public class PermissionsServiceTests +{ + [Fact(Skip = "Requires live CSOM context — covered by Plan 02 implementation")] + public async Task ScanSiteAsync_ReturnsPermissionEntries_ForMockedSite() + { + // PERM-01: ScanSiteAsync returns a list of PermissionEntry records + // Arrange — requires a real or mocked ClientContext (CSOM) + // Act + // Assert + await Task.CompletedTask; + } + + [Fact(Skip = "Requires live CSOM context — covered by Plan 02 implementation")] + public async Task ScanSiteAsync_WithIncludeInheritedFalse_SkipsItemsWithoutUniquePermissions() + { + // PERM-04: When IncludeInherited = false, items without unique permissions are excluded + // Arrange — requires a real or mocked ClientContext (CSOM) + // Act + // Assert + await Task.CompletedTask; + } +} diff --git a/SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs b/SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs new file mode 100644 index 0000000..b45b2af --- /dev/null +++ b/SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs @@ -0,0 +1,22 @@ +using SharepointToolbox.Core.Models; +using SharepointToolbox.Services; + +namespace SharepointToolbox.Tests.ViewModels; + +/// +/// Test stubs for PERM-02 (multi-site scan loop). +/// Skipped until PermissionsViewModel is implemented in Plan 02. +/// +public class PermissionsViewModelTests +{ + [Fact(Skip = "Requires live CSOM context — covered by Plan 02 implementation")] + public async Task StartScanAsync_WithMultipleSiteUrls_CallsServiceOncePerUrl() + { + // PERM-02: When the user supplies N site URLs, IPermissionsService.ScanSiteAsync + // is invoked exactly once per URL (sequential, not parallel). + // Arrange — requires PermissionsViewModel and a mock IPermissionsService + // Act + // Assert + await Task.CompletedTask; + } +} diff --git a/SharepointToolbox/Core/Helpers/PermissionEntryHelper.cs b/SharepointToolbox/Core/Helpers/PermissionEntryHelper.cs new file mode 100644 index 0000000..597cd72 --- /dev/null +++ b/SharepointToolbox/Core/Helpers/PermissionEntryHelper.cs @@ -0,0 +1,30 @@ +namespace SharepointToolbox.Core.Helpers; + +/// +/// Pure static helpers for classifying SharePoint permission entries. +/// +public static class PermissionEntryHelper +{ + /// + /// Returns true when the login name is a B2B guest (contains #EXT#). + /// + public static bool IsExternalUser(string loginName) => + loginName.Contains("#EXT#", StringComparison.OrdinalIgnoreCase); + + /// + /// Removes "Limited Access" from the supplied permission levels. + /// Returns the remaining levels; returns an empty list when all are removed. + /// + public static IReadOnlyList FilterPermissionLevels(IEnumerable levels) => + levels + .Where(l => !string.Equals(l, "Limited Access", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + /// + /// Returns true when the login name represents an internal sharing-link group + /// or the "Limited Access System Group" pseudo-principal. + /// + public static bool IsSharingLinksGroup(string loginName) => + loginName.StartsWith("SharingLinks.", StringComparison.OrdinalIgnoreCase) + || loginName.Equals("Limited Access System Group", StringComparison.OrdinalIgnoreCase); +}