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>
454 lines
18 KiB
Markdown
454 lines
18 KiB
Markdown
---
|
|
phase: 08-simplified-permissions
|
|
plan: 06
|
|
type: execute
|
|
wave: 5
|
|
depends_on: ["08-05"]
|
|
files_modified:
|
|
- SharepointToolbox.Tests/Helpers/PermissionLevelMappingTests.cs
|
|
- SharepointToolbox.Tests/Models/PermissionSummaryBuilderTests.cs
|
|
- SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs
|
|
autonomous: true
|
|
requirements:
|
|
- SIMP-01
|
|
- SIMP-02
|
|
- SIMP-03
|
|
must_haves:
|
|
truths:
|
|
- "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"
|
|
artifacts:
|
|
- path: "SharepointToolbox.Tests/Helpers/PermissionLevelMappingTests.cs"
|
|
provides: "Unit tests for permission level mapping"
|
|
contains: "class PermissionLevelMappingTests"
|
|
- path: "SharepointToolbox.Tests/Models/PermissionSummaryBuilderTests.cs"
|
|
provides: "Unit tests for summary aggregation"
|
|
contains: "class PermissionSummaryBuilderTests"
|
|
- path: "SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs"
|
|
provides: "Extended ViewModel tests for simplified mode"
|
|
contains: "IsSimplifiedMode"
|
|
key_links:
|
|
- from: "SharepointToolbox.Tests/Helpers/PermissionLevelMappingTests.cs"
|
|
to: "SharepointToolbox/Core/Helpers/PermissionLevelMapping.cs"
|
|
via: "Direct static method calls"
|
|
pattern: "PermissionLevelMapping\\.Get"
|
|
- from: "SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs"
|
|
to: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs"
|
|
via: "Test constructor + property assertions"
|
|
pattern: "IsSimplifiedMode"
|
|
---
|
|
|
|
<objective>
|
|
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
|
|
</objective>
|
|
|
|
<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>
|
|
|
|
<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
|
|
|
|
<interfaces>
|
|
<!-- Types under test -->
|
|
From PermissionLevelMapping:
|
|
```csharp
|
|
public static class PermissionLevelMapping
|
|
{
|
|
public record MappingResult(string Label, RiskLevel RiskLevel);
|
|
public static MappingResult GetMapping(string roleName);
|
|
public static IReadOnlyList<MappingResult> GetMappings(string permissionLevels);
|
|
public static RiskLevel GetHighestRisk(string permissionLevels);
|
|
public static string GetSimplifiedLabels(string permissionLevels);
|
|
}
|
|
```
|
|
|
|
From PermissionSummaryBuilder:
|
|
```csharp
|
|
public static class PermissionSummaryBuilder
|
|
{
|
|
public static IReadOnlyList<PermissionSummary> Build(IEnumerable<SimplifiedPermissionEntry> entries);
|
|
}
|
|
```
|
|
|
|
From SimplifiedPermissionEntry:
|
|
```csharp
|
|
public class SimplifiedPermissionEntry
|
|
{
|
|
public PermissionEntry Inner { get; }
|
|
public string SimplifiedLabels { get; }
|
|
public RiskLevel RiskLevel { get; }
|
|
public static IReadOnlyList<SimplifiedPermissionEntry> WrapAll(IEnumerable<PermissionEntry> entries);
|
|
}
|
|
```
|
|
|
|
<!-- Existing test pattern (from PermissionsViewModelTests.cs) -->
|
|
```csharp
|
|
public class PermissionsViewModelTests
|
|
{
|
|
[Fact]
|
|
public async Task StartScanAsync_WithMultipleSiteUrls_CallsServiceOncePerUrl()
|
|
{
|
|
var vm = new PermissionsViewModel(
|
|
mockPermissionsService.Object,
|
|
mockSiteListService.Object,
|
|
mockSessionManager.Object,
|
|
new NullLogger<FeatureViewModelBase>());
|
|
// ... test ...
|
|
}
|
|
}
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create PermissionLevelMapping and PermissionSummaryBuilder tests</name>
|
|
<files>SharepointToolbox.Tests/Helpers/PermissionLevelMappingTests.cs, SharepointToolbox.Tests/Models/PermissionSummaryBuilderTests.cs</files>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/ --filter "PermissionLevelMappingTests|PermissionSummaryBuilderTests" --no-restore 2>&1 | tail -10</automated>
|
|
</verify>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add simplified mode tests to PermissionsViewModelTests</name>
|
|
<files>SharepointToolbox.Tests/ViewModels/PermissionsViewModelTests.cs</files>
|
|
<action>
|
|
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)
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet test SharepointToolbox.Tests/ --filter "PermissionsViewModelTests" --no-restore 2>&1 | tail -10</automated>
|
|
</verify>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/08-simplified-permissions/08-06-SUMMARY.md`
|
|
</output>
|