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>
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 |
|
|
true |
|
|
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 ...
}
}
```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`