feat(18-01): models, SettingsService, OwnershipElevationService + tests
- AppSettings.AutoTakeOwnership bool property defaulting to false - PermissionEntry.WasAutoElevated optional param (default false, last position) - SettingsService.SetAutoTakeOwnershipAsync persists toggle - IOwnershipElevationService interface + OwnershipElevationService wrapping Tenant.SetSiteAdmin - SettingsViewModel.AutoTakeOwnership property loads and persists via SetAutoTakeOwnershipAsync - DI registration in App.xaml.cs (Phase 18 section) - 8 new tests: models, persistence, service, viewmodel
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
using SharepointToolbox.Services;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.Tests.Services;
|
||||||
|
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public class OwnershipElevationServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void OwnershipElevationService_ImplementsIOwnershipElevationService()
|
||||||
|
{
|
||||||
|
var service = new OwnershipElevationService();
|
||||||
|
Assert.IsAssignableFrom<IOwnershipElevationService>(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AppSettings_AutoTakeOwnership_DefaultsFalse()
|
||||||
|
{
|
||||||
|
var settings = new SharepointToolbox.Core.Models.AppSettings();
|
||||||
|
Assert.False(settings.AutoTakeOwnership);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AppSettings_AutoTakeOwnership_RoundTripsThroughJson()
|
||||||
|
{
|
||||||
|
var json = System.Text.Json.JsonSerializer.Serialize(
|
||||||
|
new SharepointToolbox.Core.Models.AppSettings { AutoTakeOwnership = true },
|
||||||
|
new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase });
|
||||||
|
|
||||||
|
var loaded = System.Text.Json.JsonSerializer.Deserialize<SharepointToolbox.Core.Models.AppSettings>(json,
|
||||||
|
new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase });
|
||||||
|
|
||||||
|
Assert.NotNull(loaded);
|
||||||
|
Assert.True(loaded!.AutoTakeOwnership);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PermissionEntry_WasAutoElevated_DefaultsFalse()
|
||||||
|
{
|
||||||
|
var entry = new SharepointToolbox.Core.Models.PermissionEntry(
|
||||||
|
"Site", "Title", "https://example.com", false,
|
||||||
|
"User", "user@example.com", "Read", "Direct Permissions", "User");
|
||||||
|
|
||||||
|
Assert.False(entry.WasAutoElevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PermissionEntry_WasAutoElevated_TrueWhenSet()
|
||||||
|
{
|
||||||
|
var entry = new SharepointToolbox.Core.Models.PermissionEntry(
|
||||||
|
"Site", "Title", "https://example.com", false,
|
||||||
|
"User", "user@example.com", "Read", "Direct Permissions", "User",
|
||||||
|
WasAutoElevated: true);
|
||||||
|
|
||||||
|
Assert.True(entry.WasAutoElevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PermissionEntry_WithExpression_CopiesWasAutoElevated()
|
||||||
|
{
|
||||||
|
var original = new SharepointToolbox.Core.Models.PermissionEntry(
|
||||||
|
"Site", "Title", "https://example.com", false,
|
||||||
|
"User", "user@example.com", "Read", "Direct Permissions", "User");
|
||||||
|
|
||||||
|
var elevated = original with { WasAutoElevated = true };
|
||||||
|
|
||||||
|
Assert.False(original.WasAutoElevated);
|
||||||
|
Assert.True(elevated.WasAutoElevated);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System.IO;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Moq;
|
||||||
|
using SharepointToolbox.Infrastructure.Persistence;
|
||||||
|
using SharepointToolbox.Services;
|
||||||
|
using SharepointToolbox.ViewModels;
|
||||||
|
using SharepointToolbox.ViewModels.Tabs;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.Tests.ViewModels;
|
||||||
|
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public class SettingsViewModelOwnershipTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _tempFile;
|
||||||
|
|
||||||
|
public SettingsViewModelOwnershipTests()
|
||||||
|
{
|
||||||
|
_tempFile = Path.GetTempFileName();
|
||||||
|
File.Delete(_tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (File.Exists(_tempFile)) File.Delete(_tempFile);
|
||||||
|
if (File.Exists(_tempFile + ".tmp")) File.Delete(_tempFile + ".tmp");
|
||||||
|
}
|
||||||
|
|
||||||
|
private SettingsViewModel CreateViewModel()
|
||||||
|
{
|
||||||
|
var settingsService = new SettingsService(new SettingsRepository(_tempFile));
|
||||||
|
var mockBranding = new Mock<IBrandingService>().Object;
|
||||||
|
var logger = NullLogger<FeatureViewModelBase>.Instance;
|
||||||
|
return new SettingsViewModel(settingsService, mockBranding, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task LoadAsync_AutoTakeOwnership_LoadsFalseByDefault()
|
||||||
|
{
|
||||||
|
var vm = CreateViewModel();
|
||||||
|
|
||||||
|
await vm.LoadAsync();
|
||||||
|
|
||||||
|
Assert.False(vm.AutoTakeOwnership);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetAutoTakeOwnership_True_CallsSetAutoTakeOwnershipAsync()
|
||||||
|
{
|
||||||
|
// Use a real SettingsService backed by temp file to verify persistence
|
||||||
|
var settingsService = new SettingsService(new SettingsRepository(_tempFile));
|
||||||
|
var mockBranding = new Mock<IBrandingService>().Object;
|
||||||
|
var logger = NullLogger<FeatureViewModelBase>.Instance;
|
||||||
|
var vm = new SettingsViewModel(settingsService, mockBranding, logger);
|
||||||
|
|
||||||
|
await vm.LoadAsync();
|
||||||
|
vm.AutoTakeOwnership = true;
|
||||||
|
|
||||||
|
// Small delay to let the fire-and-forget persist
|
||||||
|
await Task.Delay(100);
|
||||||
|
|
||||||
|
var persisted = await settingsService.GetSettingsAsync();
|
||||||
|
Assert.True(persisted.AutoTakeOwnership);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -161,6 +161,9 @@ public partial class App : Application
|
|||||||
// Phase 17: Group Expansion
|
// Phase 17: Group Expansion
|
||||||
services.AddTransient<ISharePointGroupResolver, SharePointGroupResolver>();
|
services.AddTransient<ISharePointGroupResolver, SharePointGroupResolver>();
|
||||||
|
|
||||||
|
// Phase 18: Auto-Take Ownership
|
||||||
|
services.AddTransient<IOwnershipElevationService, OwnershipElevationService>();
|
||||||
|
|
||||||
// Phase 7: User Access Audit
|
// Phase 7: User Access Audit
|
||||||
services.AddTransient<IUserAccessAuditService, UserAccessAuditService>();
|
services.AddTransient<IUserAccessAuditService, UserAccessAuditService>();
|
||||||
services.AddTransient<IGraphUserSearchService, GraphUserSearchService>();
|
services.AddTransient<IGraphUserSearchService, GraphUserSearchService>();
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ public class AppSettings
|
|||||||
{
|
{
|
||||||
public string DataFolder { get; set; } = string.Empty;
|
public string DataFolder { get; set; } = string.Empty;
|
||||||
public string Lang { get; set; } = "en";
|
public string Lang { get; set; } = "en";
|
||||||
|
public bool AutoTakeOwnership { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ public record PermissionEntry(
|
|||||||
string UserLogins, // Semicolon-joined login names
|
string UserLogins, // Semicolon-joined login names
|
||||||
string PermissionLevels, // Semicolon-joined role names (Limited Access already removed)
|
string PermissionLevels, // Semicolon-joined role names (Limited Access already removed)
|
||||||
string GrantedThrough, // "Direct Permissions" | "SharePoint Group: <name>"
|
string GrantedThrough, // "Direct Permissions" | "SharePoint Group: <name>"
|
||||||
string PrincipalType // "SharePointGroup" | "User" | "External User"
|
string PrincipalType, // "SharePointGroup" | "User" | "External User"
|
||||||
|
bool WasAutoElevated = false // Set to true when site admin was auto-granted to access this entry
|
||||||
);
|
);
|
||||||
|
|||||||
8
SharepointToolbox/Services/IOwnershipElevationService.cs
Normal file
8
SharepointToolbox/Services/IOwnershipElevationService.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.SharePoint.Client;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.Services;
|
||||||
|
|
||||||
|
public interface IOwnershipElevationService
|
||||||
|
{
|
||||||
|
Task ElevateAsync(ClientContext tenantAdminCtx, string siteUrl, string loginName, CancellationToken ct);
|
||||||
|
}
|
||||||
14
SharepointToolbox/Services/OwnershipElevationService.cs
Normal file
14
SharepointToolbox/Services/OwnershipElevationService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.Online.SharePoint.TenantAdministration;
|
||||||
|
using Microsoft.SharePoint.Client;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.Services;
|
||||||
|
|
||||||
|
public class OwnershipElevationService : IOwnershipElevationService
|
||||||
|
{
|
||||||
|
public async Task ElevateAsync(ClientContext tenantAdminCtx, string siteUrl, string loginName, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var tenant = new Tenant(tenantAdminCtx);
|
||||||
|
tenant.SetSiteAdmin(siteUrl, loginName, isSiteAdmin: true);
|
||||||
|
await tenantAdminCtx.ExecuteQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,4 +36,11 @@ public class SettingsService
|
|||||||
settings.DataFolder = path;
|
settings.DataFolder = path;
|
||||||
await _repository.SaveAsync(settings);
|
await _repository.SaveAsync(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetAutoTakeOwnershipAsync(bool enabled)
|
||||||
|
{
|
||||||
|
var settings = await _repository.LoadAsync();
|
||||||
|
settings.AutoTakeOwnership = enabled;
|
||||||
|
await _repository.SaveAsync(settings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,19 @@ public partial class SettingsViewModel : FeatureViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _autoTakeOwnership;
|
||||||
|
public bool AutoTakeOwnership
|
||||||
|
{
|
||||||
|
get => _autoTakeOwnership;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_autoTakeOwnership == value) return;
|
||||||
|
_autoTakeOwnership = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
_ = _settingsService.SetAutoTakeOwnershipAsync(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string? _mspLogoPreview;
|
private string? _mspLogoPreview;
|
||||||
public string? MspLogoPreview
|
public string? MspLogoPreview
|
||||||
{
|
{
|
||||||
@@ -65,8 +78,10 @@ public partial class SettingsViewModel : FeatureViewModelBase
|
|||||||
var settings = await _settingsService.GetSettingsAsync();
|
var settings = await _settingsService.GetSettingsAsync();
|
||||||
_selectedLanguage = settings.Lang;
|
_selectedLanguage = settings.Lang;
|
||||||
_dataFolder = settings.DataFolder;
|
_dataFolder = settings.DataFolder;
|
||||||
|
_autoTakeOwnership = settings.AutoTakeOwnership;
|
||||||
OnPropertyChanged(nameof(SelectedLanguage));
|
OnPropertyChanged(nameof(SelectedLanguage));
|
||||||
OnPropertyChanged(nameof(DataFolder));
|
OnPropertyChanged(nameof(DataFolder));
|
||||||
|
OnPropertyChanged(nameof(AutoTakeOwnership));
|
||||||
|
|
||||||
var mspLogo = await _brandingService.GetMspLogoAsync();
|
var mspLogo = await _brandingService.GetMspLogoAsync();
|
||||||
MspLogoPreview = mspLogo is not null ? $"data:{mspLogo.MimeType};base64,{mspLogo.Base64}" : null;
|
MspLogoPreview = mspLogo is not null ? $"data:{mspLogo.MimeType};base64,{mspLogo.Base64}" : null;
|
||||||
|
|||||||
Reference in New Issue
Block a user