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:
@@ -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.Core.Models;
|
||||||
using SharepointToolbox.Services;
|
using SharepointToolbox.Services;
|
||||||
|
using SharepointToolbox.ViewModels.Tabs;
|
||||||
|
|
||||||
namespace SharepointToolbox.Tests.ViewModels;
|
namespace SharepointToolbox.Tests.ViewModels;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Test stubs for PERM-02 (multi-site scan loop).
|
/// Unit tests for PermissionsViewModel.
|
||||||
/// Skipped until PermissionsViewModel is implemented in Plan 02.
|
/// PERM-02: multi-site scan loop invokes ScanSiteAsync once per URL.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PermissionsViewModelTests
|
public class PermissionsViewModelTests
|
||||||
{
|
{
|
||||||
[Fact(Skip = "Requires live CSOM context — covered by Plan 02 implementation")]
|
[Fact]
|
||||||
public async Task StartScanAsync_WithMultipleSiteUrls_CallsServiceOncePerUrl()
|
public async Task StartScanAsync_WithMultipleSiteUrls_CallsServiceOncePerUrl()
|
||||||
{
|
{
|
||||||
// PERM-02: When the user supplies N site URLs, IPermissionsService.ScanSiteAsync
|
// Arrange
|
||||||
// is invoked exactly once per URL (sequential, not parallel).
|
var mockPermissionsService = new Mock<IPermissionsService>();
|
||||||
// Arrange — requires PermissionsViewModel and a 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
|
// Act
|
||||||
// Assert
|
await vm.TestRunOperationAsync(CancellationToken.None, new Progress<OperationProgress>());
|
||||||
await Task.CompletedTask;
|
|
||||||
|
// 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
SharepointToolbox/Services/ISessionManager.cs
Normal file
20
SharepointToolbox/Services/ISessionManager.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ namespace SharepointToolbox.Services;
|
|||||||
/// Every SharePoint operation goes through this class — callers MUST NOT store
|
/// Every SharePoint operation goes through this class — callers MUST NOT store
|
||||||
/// ClientContext references; request it fresh each time via GetOrCreateContextAsync.
|
/// ClientContext references; request it fresh each time via GetOrCreateContextAsync.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SessionManager
|
public class SessionManager : ISessionManager
|
||||||
{
|
{
|
||||||
private readonly MsalClientFactory _msalFactory;
|
private readonly MsalClientFactory _msalFactory;
|
||||||
private readonly Dictionary<string, ClientContext> _contexts = new();
|
private readonly Dictionary<string, ClientContext> _contexts = new();
|
||||||
|
|||||||
47
SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs
Normal file
47
SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user