test(02-06): add failing test for PermissionsViewModel multi-site scan

- Write StartScanAsync_WithMultipleSiteUrls_CallsServiceOncePerUrl test (RED)
- Create ISessionManager interface for testability
- Implement ISessionManager on SessionManager
- Add PermissionsViewModel stub (NotImplementedException) to satisfy compile
This commit is contained in:
Dev
2026-04-02 14:04:22 +02:00
parent 48ccf5891b
commit c462a0b310
4 changed files with 121 additions and 9 deletions

View File

@@ -1,22 +1,67 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SharePoint.Client;
using Moq;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Services;
using SharepointToolbox.ViewModels.Tabs;
namespace SharepointToolbox.Tests.ViewModels;
/// <summary>
/// Test stubs for PERM-02 (multi-site scan loop).
/// Skipped until PermissionsViewModel is implemented in Plan 02.
/// Unit tests for PermissionsViewModel.
/// PERM-02: multi-site scan loop invokes ScanSiteAsync once per URL.
/// </summary>
public class PermissionsViewModelTests
{
[Fact(Skip = "Requires live CSOM context — covered by Plan 02 implementation")]
[Fact]
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
// Arrange
var mockPermissionsService = new Mock<IPermissionsService>();
mockPermissionsService
.Setup(s => s.ScanSiteAsync(
It.IsAny<ClientContext>(),
It.IsAny<ScanOptions>(),
It.IsAny<IProgress<OperationProgress>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<PermissionEntry>());
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<PermissionsViewModel>());
// Set up two site URLs via SelectedSites
vm.SelectedSites.Add(new SiteInfo("https://tenant1.sharepoint.com/sites/alpha", "Alpha"));
vm.SelectedSites.Add(new SiteInfo("https://tenant1.sharepoint.com/sites/beta", "Beta"));
vm.SetCurrentProfile(new TenantProfile
{
Name = "Test",
TenantUrl = "https://tenant1.sharepoint.com",
ClientId = "client-id"
});
// Act
// Assert
await Task.CompletedTask;
await vm.TestRunOperationAsync(CancellationToken.None, new Progress<OperationProgress>());
// Assert: ScanSiteAsync called exactly twice (once per URL)
mockPermissionsService.Verify(
s => s.ScanSiteAsync(
It.IsAny<ClientContext>(),
It.IsAny<ScanOptions>(),
It.IsAny<IProgress<OperationProgress>>(),
It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.SharePoint.Client;
using SharepointToolbox.Core.Models;
namespace SharepointToolbox.Services;
/// <summary>
/// Abstraction over SessionManager, enabling unit testing of ViewModels
/// without live MSAL/SharePoint connections.
/// </summary>
public interface ISessionManager
{
/// <summary>Returns an existing ClientContext or creates one via interactive login.</summary>
Task<ClientContext> GetOrCreateContextAsync(TenantProfile profile, CancellationToken ct = default);
/// <summary>Clears the cached session for the given tenant URL.</summary>
Task ClearSessionAsync(string tenantUrl);
/// <summary>Returns true if an authenticated ClientContext exists for this tenantUrl.</summary>
bool IsAuthenticated(string tenantUrl);
}

View File

@@ -14,7 +14,7 @@ namespace SharepointToolbox.Services;
/// Every SharePoint operation goes through this class — callers MUST NOT store
/// ClientContext references; request it fresh each time via GetOrCreateContextAsync.
/// </summary>
public class SessionManager
public class SessionManager : ISessionManager
{
private readonly MsalClientFactory _msalFactory;
private readonly Dictionary<string, ClientContext> _contexts = new();

View File

@@ -0,0 +1,47 @@
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Services;
namespace SharepointToolbox.ViewModels.Tabs;
/// <summary>
/// STUB: PermissionsViewModel — RED phase. Not yet implemented.
/// </summary>
public partial class PermissionsViewModel : FeatureViewModelBase
{
private readonly IPermissionsService _permissionsService;
private readonly ISiteListService _siteListService;
private readonly ISessionManager _sessionManager;
public ObservableCollection<SiteInfo> SelectedSites { get; } = new();
public ObservableCollection<PermissionEntry> Results { get; private set; } = new();
internal TenantProfile? _currentProfile;
public PermissionsViewModel(
IPermissionsService permissionsService,
ISiteListService siteListService,
ISessionManager sessionManager,
ILogger<PermissionsViewModel> logger)
: base(logger)
{
_permissionsService = permissionsService;
_siteListService = siteListService;
_sessionManager = sessionManager;
}
public void SetCurrentProfile(TenantProfile profile) => _currentProfile = profile;
internal Task TestRunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
=> RunOperationAsync(ct, progress);
protected override async Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
{
// RED STUB: always throws to make tests fail at RED phase
throw new NotImplementedException("PermissionsViewModel.RunOperationAsync not yet implemented.");
}
}