Files
Sharepoint-Toolbox/.planning/phases/08-simplified-permissions/08-06-PLAN.md
Dev c871effa87 docs(08-simplified-permissions): create phase plan (6 plans, 5 waves)
Plans cover plain-language permission labels, risk-level color coding,
summary counts, detail-level toggle, export integration, and unit tests.
PermissionEntry record is NOT modified — uses wrapper pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:00:08 +02:00

18 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
08-simplified-permissions 06 execute 5
08-05
SharepointToolbox.Tests/Helpers/PermissionLevelMappingTests.cs
SharepointToolbox.Tests/Models/PermissionSummaryBuilderTests.cs
SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs
true
SIMP-01
SIMP-02
SIMP-03
truths artifacts key_links
PermissionLevelMapping maps all known role names correctly and handles unknown roles
PermissionSummaryBuilder produces 4 risk-level groups with correct counts
PermissionsViewModel toggle behavior is verified: IsSimplifiedMode rebuilds data, IsDetailView switches without re-scan
SimplifiedPermissionEntry wraps PermissionEntry correctly with computed labels and risk levels
path provides contains
SharepointToolbox.Tests/Helpers/PermissionLevelMappingTests.cs Unit tests for permission level mapping class PermissionLevelMappingTests
path provides contains
SharepointToolbox.Tests/Models/PermissionSummaryBuilderTests.cs Unit tests for summary aggregation class PermissionSummaryBuilderTests
path provides contains
SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs Extended ViewModel tests for simplified mode IsSimplifiedMode
from to via pattern
SharepointToolbox.Tests/Helpers/PermissionLevelMappingTests.cs SharepointToolbox/Core/Helpers/PermissionLevelMapping.cs Direct static method calls PermissionLevelMapping.Get
from to via pattern
SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs Test constructor + property assertions IsSimplifiedMode
Add unit tests for the simplified permissions feature: PermissionLevelMapping, PermissionSummaryBuilder, SimplifiedPermissionEntry wrapping, and PermissionsViewModel toggle behavior.

Purpose: Validates the core logic of all three SIMP requirements. Mapping correctness (SIMP-01), summary aggregation (SIMP-02), and toggle-without-rescan behavior (SIMP-03). Output: PermissionLevelMappingTests.cs, PermissionSummaryBuilderTests.cs, updated PermissionsViewModelTests.cs

<execution_context> @C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/dev/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/08-simplified-permissions/08-01-SUMMARY.md @.planning/phases/08-simplified-permissions/08-02-SUMMARY.md From PermissionLevelMapping: ```csharp public static class PermissionLevelMapping { public record MappingResult(string Label, RiskLevel RiskLevel); public static MappingResult GetMapping(string roleName); public static IReadOnlyList GetMappings(string permissionLevels); public static RiskLevel GetHighestRisk(string permissionLevels); public static string GetSimplifiedLabels(string permissionLevels); } ```

From PermissionSummaryBuilder:

public static class PermissionSummaryBuilder
{
    public static IReadOnlyList<PermissionSummary> Build(IEnumerable<SimplifiedPermissionEntry> entries);
}

From SimplifiedPermissionEntry:

public class SimplifiedPermissionEntry
{
    public PermissionEntry Inner { get; }
    public string SimplifiedLabels { get; }
    public RiskLevel RiskLevel { get; }
    public static IReadOnlyList<SimplifiedPermissionEntry> WrapAll(IEnumerable<PermissionEntry> entries);
}
public class PermissionsViewModelTests
{
    [Fact]
    public async Task StartScanAsync_WithMultipleSiteUrls_CallsServiceOncePerUrl()
    {
        var vm = new PermissionsViewModel(
            mockPermissionsService.Object,
            mockSiteListService.Object,
            mockSessionManager.Object,
            new NullLogger<FeatureViewModelBase>());
        // ... test ...
    }
}
Task 1: Create PermissionLevelMapping and PermissionSummaryBuilder tests SharepointToolbox.Tests/Helpers/PermissionLevelMappingTests.cs, SharepointToolbox.Tests/Models/PermissionSummaryBuilderTests.cs Create `SharepointToolbox.Tests/Helpers/PermissionLevelMappingTests.cs`:
```csharp
using SharepointToolbox.Core.Helpers;
using SharepointToolbox.Core.Models;

namespace SharepointToolbox.Tests.Helpers;

public class PermissionLevelMappingTests
{
    [Theory]
    [InlineData("Full Control", RiskLevel.High)]
    [InlineData("Site Collection Administrator", RiskLevel.High)]
    [InlineData("Contribute", RiskLevel.Medium)]
    [InlineData("Edit", RiskLevel.Medium)]
    [InlineData("Design", RiskLevel.Medium)]
    [InlineData("Approve", RiskLevel.Medium)]
    [InlineData("Manage Hierarchy", RiskLevel.Medium)]
    [InlineData("Read", RiskLevel.Low)]
    [InlineData("Restricted Read", RiskLevel.Low)]
    [InlineData("View Only", RiskLevel.ReadOnly)]
    [InlineData("Restricted View", RiskLevel.ReadOnly)]
    public void GetMapping_KnownRoles_ReturnsCorrectRiskLevel(string roleName, RiskLevel expected)
    {
        var result = PermissionLevelMapping.GetMapping(roleName);
        Assert.Equal(expected, result.RiskLevel);
        Assert.NotEmpty(result.Label);
    }

    [Fact]
    public void GetMapping_UnknownRole_ReturnsMediumRiskWithRawName()
    {
        var result = PermissionLevelMapping.GetMapping("Custom Permission Level");
        Assert.Equal(RiskLevel.Medium, result.RiskLevel);
        Assert.Equal("Custom Permission Level", result.Label);
    }

    [Fact]
    public void GetMapping_CaseInsensitive()
    {
        var lower = PermissionLevelMapping.GetMapping("full control");
        var upper = PermissionLevelMapping.GetMapping("FULL CONTROL");
        Assert.Equal(RiskLevel.High, lower.RiskLevel);
        Assert.Equal(RiskLevel.High, upper.RiskLevel);
    }

    [Fact]
    public void GetMappings_SemicolonDelimited_SplitsAndMaps()
    {
        var results = PermissionLevelMapping.GetMappings("Full Control; Read");
        Assert.Equal(2, results.Count);
        Assert.Equal(RiskLevel.High, results[0].RiskLevel);
        Assert.Equal(RiskLevel.Low, results[1].RiskLevel);
    }

    [Fact]
    public void GetMappings_EmptyString_ReturnsEmpty()
    {
        var results = PermissionLevelMapping.GetMappings("");
        Assert.Empty(results);
    }

    [Fact]
    public void GetHighestRisk_MultipleLevels_ReturnsHighest()
    {
        // Full Control (High) + Read (Low) => High
        var risk = PermissionLevelMapping.GetHighestRisk("Full Control; Read");
        Assert.Equal(RiskLevel.High, risk);
    }

    [Fact]
    public void GetHighestRisk_SingleReadOnly_ReturnsReadOnly()
    {
        var risk = PermissionLevelMapping.GetHighestRisk("View Only");
        Assert.Equal(RiskLevel.ReadOnly, risk);
    }

    [Fact]
    public void GetSimplifiedLabels_JoinsLabels()
    {
        var labels = PermissionLevelMapping.GetSimplifiedLabels("Contribute; Read");
        Assert.Contains("Can edit files and list items", labels);
        Assert.Contains("Can view files and pages", labels);
    }
}
```

Create `SharepointToolbox.Tests/Models/PermissionSummaryBuilderTests.cs`:

```csharp
using SharepointToolbox.Core.Models;

namespace SharepointToolbox.Tests.Models;

public class PermissionSummaryBuilderTests
{
    private static PermissionEntry MakeEntry(string permLevels, string users = "User1", string logins = "user1@test.com") =>
        new PermissionEntry(
            ObjectType: "Site",
            Title: "Test",
            Url: "https://test.sharepoint.com",
            HasUniquePermissions: true,
            Users: users,
            UserLogins: logins,
            PermissionLevels: permLevels,
            GrantedThrough: "Direct Permissions",
            PrincipalType: "User");

    [Fact]
    public void Build_ReturnsAllFourRiskLevels()
    {
        var entries = SimplifiedPermissionEntry.WrapAll(new[]
        {
            MakeEntry("Full Control"),
            MakeEntry("Contribute"),
            MakeEntry("Read"),
            MakeEntry("View Only")
        });

        var summaries = PermissionSummaryBuilder.Build(entries);

        Assert.Equal(4, summaries.Count);
        Assert.Contains(summaries, s => s.RiskLevel == RiskLevel.High && s.Count == 1);
        Assert.Contains(summaries, s => s.RiskLevel == RiskLevel.Medium && s.Count == 1);
        Assert.Contains(summaries, s => s.RiskLevel == RiskLevel.Low && s.Count == 1);
        Assert.Contains(summaries, s => s.RiskLevel == RiskLevel.ReadOnly && s.Count == 1);
    }

    [Fact]
    public void Build_EmptyCollection_ReturnsZeroCounts()
    {
        var summaries = PermissionSummaryBuilder.Build(Array.Empty<SimplifiedPermissionEntry>());

        Assert.Equal(4, summaries.Count);
        Assert.All(summaries, s => Assert.Equal(0, s.Count));
    }

    [Fact]
    public void Build_CountsDistinctUsers()
    {
        var entries = SimplifiedPermissionEntry.WrapAll(new[]
        {
            MakeEntry("Full Control", "Alice", "alice@test.com"),
            MakeEntry("Full Control", "Bob", "bob@test.com"),
            MakeEntry("Full Control", "Alice", "alice@test.com"),  // duplicate user
        });

        var summaries = PermissionSummaryBuilder.Build(entries);
        var high = summaries.Single(s => s.RiskLevel == RiskLevel.High);

        Assert.Equal(3, high.Count);          // 3 entries
        Assert.Equal(2, high.DistinctUsers);   // 2 distinct users
    }

    [Fact]
    public void SimplifiedPermissionEntry_WrapAll_PreservesInner()
    {
        var original = MakeEntry("Contribute");
        var wrapped = SimplifiedPermissionEntry.WrapAll(new[] { original });

        Assert.Single(wrapped);
        Assert.Same(original, wrapped[0].Inner);
        Assert.Equal("Contribute", wrapped[0].PermissionLevels);
        Assert.Equal(RiskLevel.Medium, wrapped[0].RiskLevel);
        Assert.Contains("Can edit", wrapped[0].SimplifiedLabels);
    }
}
```

Create the `SharepointToolbox.Tests/Helpers/` and `SharepointToolbox.Tests/Models/` directories if they don't exist.
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/ --filter "PermissionLevelMappingTests|PermissionSummaryBuilderTests" --no-restore 2>&1 | tail -10 PermissionLevelMappingTests covers: all 11 known roles, unknown role fallback, case insensitivity, semicolon splitting, highest risk, simplified labels. PermissionSummaryBuilderTests covers: 4 risk levels, empty input, distinct user counting, SimplifiedPermissionEntry wrapping. All tests pass. Task 2: Add simplified mode tests to PermissionsViewModelTests SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs Add the following test methods to the existing `PermissionsViewModelTests` class in `SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs`. Add any needed using statements at the top:
```csharp
using CommunityToolkit.Mvvm.Messaging;
```

Add a helper method and new tests after the existing test:

```csharp
/// <summary>
/// Creates a PermissionsViewModel with mocked services and pre-populated results.
/// </summary>
private static PermissionsViewModel CreateViewModelWithResults(IReadOnlyList<PermissionEntry> results)
{
    var mockPermissionsService = new Mock<IPermissionsService>();
    mockPermissionsService
        .Setup(s => s.ScanSiteAsync(
            It.IsAny<ClientContext>(),
            It.IsAny<ScanOptions>(),
            It.IsAny<IProgress<OperationProgress>>(),
            It.IsAny<CancellationToken>()))
        .ReturnsAsync(results.ToList());

    var mockSiteListService = new Mock<ISiteListService>();

    var mockSessionManager = new Mock<ISessionManager>();
    mockSessionManager
        .Setup(s => s.GetOrCreateContextAsync(It.IsAny<TenantProfile>(), It.IsAny<CancellationToken>()))
        .ReturnsAsync((ClientContext)null!);

    var vm = new PermissionsViewModel(
        mockPermissionsService.Object,
        mockSiteListService.Object,
        mockSessionManager.Object,
        new NullLogger<FeatureViewModelBase>());

    return vm;
}

[Fact]
public void IsSimplifiedMode_Default_IsFalse()
{
    WeakReferenceMessenger.Default.Reset();
    var vm = CreateViewModelWithResults(Array.Empty<PermissionEntry>());
    Assert.False(vm.IsSimplifiedMode);
}

[Fact]
public async Task IsSimplifiedMode_WhenToggled_RebuildSimplifiedResults()
{
    WeakReferenceMessenger.Default.Reset();
    var entries = new List<PermissionEntry>
    {
        new("Site", "Test", "https://test.sharepoint.com", true, "User1", "user1@test.com", "Full Control", "Direct Permissions", "User"),
        new("List", "Docs", "https://test.sharepoint.com/docs", false, "User2", "user2@test.com", "Read", "Direct Permissions", "User"),
    };

    var vm = CreateViewModelWithResults(entries);

    // Simulate scan completing
    vm.SelectedSites.Add(new SiteInfo("https://test.sharepoint.com", "Test"));
    vm.SetCurrentProfile(new TenantProfile { Name = "Test", TenantUrl = "https://test.sharepoint.com", ClientId = "cid" });
    await vm.TestRunOperationAsync(CancellationToken.None, new Progress<OperationProgress>());

    // Before toggle: simplified results empty
    Assert.Empty(vm.SimplifiedResults);

    // Toggle on
    vm.IsSimplifiedMode = true;

    // After toggle: simplified results populated
    Assert.Equal(2, vm.SimplifiedResults.Count);
    Assert.Equal(4, vm.Summaries.Count);
}

[Fact]
public async Task IsDetailView_Toggle_DoesNotChangeCounts()
{
    WeakReferenceMessenger.Default.Reset();
    var entries = new List<PermissionEntry>
    {
        new("Site", "Test", "https://test.sharepoint.com", true, "User1", "user1@test.com", "Contribute", "Direct Permissions", "User"),
    };

    var vm = CreateViewModelWithResults(entries);
    vm.SelectedSites.Add(new SiteInfo("https://test.sharepoint.com", "Test"));
    vm.SetCurrentProfile(new TenantProfile { Name = "Test", TenantUrl = "https://test.sharepoint.com", ClientId = "cid" });
    await vm.TestRunOperationAsync(CancellationToken.None, new Progress<OperationProgress>());

    vm.IsSimplifiedMode = true;
    var countBefore = vm.SimplifiedResults.Count;

    vm.IsDetailView = false;
    Assert.Equal(countBefore, vm.SimplifiedResults.Count);  // No re-computation

    vm.IsDetailView = true;
    Assert.Equal(countBefore, vm.SimplifiedResults.Count);  // Still the same
}

[Fact]
public async Task Summaries_ContainsCorrectRiskBreakdown()
{
    WeakReferenceMessenger.Default.Reset();
    var entries = new List<PermissionEntry>
    {
        new("Site", "S1", "https://s1", true, "Admin", "admin@t.com", "Full Control", "Direct", "User"),
        new("Site", "S2", "https://s2", true, "Editor", "ed@t.com", "Contribute", "Direct", "User"),
        new("List", "L1", "https://l1", false, "Reader", "read@t.com", "Read", "Direct", "User"),
    };

    var vm = CreateViewModelWithResults(entries);
    vm.SelectedSites.Add(new SiteInfo("https://s1", "S1"));
    vm.SetCurrentProfile(new TenantProfile { Name = "Test", TenantUrl = "https://s1", ClientId = "cid" });
    await vm.TestRunOperationAsync(CancellationToken.None, new Progress<OperationProgress>());

    vm.IsSimplifiedMode = true;

    var high = vm.Summaries.Single(s => s.RiskLevel == RiskLevel.High);
    var medium = vm.Summaries.Single(s => s.RiskLevel == RiskLevel.Medium);
    var low = vm.Summaries.Single(s => s.RiskLevel == RiskLevel.Low);

    Assert.Equal(1, high.Count);
    Assert.Equal(1, medium.Count);
    Assert.Equal(1, low.Count);
}
```

Add the RiskLevel using statement:
```csharp
using SharepointToolbox.Core.Models;  // Already present (for PermissionEntry)
```
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/ --filter "PermissionsViewModelTests" --no-restore 2>&1 | tail -10 PermissionsViewModelTests has 5 tests total (1 existing + 4 new). Tests verify: IsSimplifiedMode default false, toggle rebuilds SimplifiedResults, IsDetailView toggle doesn't re-compute, Summaries has correct risk breakdown. All tests pass. - `dotnet test SharepointToolbox.Tests/ --no-restore` passes all tests - PermissionLevelMappingTests: 9 test methods covering known roles, unknown fallback, case insensitivity, splitting, risk ranking - PermissionSummaryBuilderTests: 4 test methods covering risk levels, empty input, distinct users, wrapping - PermissionsViewModelTests: 5 test methods (1 existing + 4 new) covering simplified mode toggle, detail toggle, summary breakdown

<success_criteria> All simplified permissions logic is covered by automated tests. Mapping correctness (SIMP-01), summary aggregation (SIMP-02), and toggle-without-rescan behavior (SIMP-03) are all verified. The test suite catches regressions in the core mapping layer and ViewModel behavior. </success_criteria>

After completion, create `.planning/phases/08-simplified-permissions/08-06-SUMMARY.md`