From a9f6bde6867a3301477debe2a4cf3f9c7e5c3dc0 Mon Sep 17 00:00:00 2001 From: Dev Date: Thu, 2 Apr 2026 13:50:41 +0200 Subject: [PATCH] test(02-01): scaffold PermissionsService, ViewModel, and classification test stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PermissionEntryHelper.cs: pure static IsExternalUser, FilterPermissionLevels, IsSharingLinksGroup - PermissionEntryClassificationTests.cs: 7 real [Fact] tests — all passing immediately - PermissionsServiceTests.cs: 2 stubs (PERM-01, PERM-04) skipped until Plan 02 CSOM impl - PermissionsViewModelTests.cs: 1 stub (PERM-02) skipped until Plan 02 ViewModel impl --- .../PermissionEntryClassificationTests.cs | 63 +++++++++++++++++++ .../Services/PermissionsServiceTests.cs | 31 +++++++++ .../ViewModels/PermissionsViewModelTests.cs | 22 +++++++ .../Core/Helpers/PermissionEntryHelper.cs | 30 +++++++++ 4 files changed, 146 insertions(+) create mode 100644 SharepointToolbox.Tests/Services/PermissionEntryClassificationTests.cs create mode 100644 SharepointToolbox.Tests/Services/PermissionsServiceTests.cs create mode 100644 SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs create mode 100644 SharepointToolbox/Core/Helpers/PermissionEntryHelper.cs 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); +}