test(06-05): add GlobalSiteSelectionTests with 10 passing tests
- Message broadcast: GlobalSitesChangedMessage carries site list to receivers - Base class: FeatureViewModelBase.GlobalSites updated on message receive - Storage tab: SiteUrl pre-filled from first global site - Storage tab: local override prevents global from overwriting SiteUrl - Storage tab: clearing SiteUrl reverts to global site (override reset) - Permissions tab: SelectedSites pre-populated from global sites - Permissions tab: local picker override blocks subsequent global updates - Tenant switch: resets local override so new global sites apply cleanly - Transfer tab: SourceSiteUrl pre-filled from first global site - MainWindowViewModel: GlobalSitesSelectedLabel reflects site count
This commit is contained in:
323
SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs
Normal file
323
SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using SharepointToolbox.Core.Messages;
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Infrastructure.Auth;
|
||||
using SharepointToolbox.Infrastructure.Persistence;
|
||||
using SharepointToolbox.Services;
|
||||
using SharepointToolbox.Services.Export;
|
||||
using SharepointToolbox.ViewModels;
|
||||
using SharepointToolbox.ViewModels.Tabs;
|
||||
|
||||
namespace SharepointToolbox.Tests.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the global site selection flow (Phase 6).
|
||||
/// Covers: message broadcast, base class reception, single-site pre-fill,
|
||||
/// multi-site pre-populate, local override, override reset, tenant switch clear,
|
||||
/// and toolbar label update.
|
||||
/// Requirements: SITE-01, SITE-02
|
||||
/// </summary>
|
||||
public class GlobalSiteSelectionTests
|
||||
{
|
||||
// ── Helper: minimal concrete subclass of FeatureViewModelBase ────────────
|
||||
|
||||
private class TestFeatureViewModel : FeatureViewModelBase
|
||||
{
|
||||
public TestFeatureViewModel(ILogger<FeatureViewModelBase> logger) : base(logger) { }
|
||||
|
||||
protected override Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>Expose protected GlobalSites for assertions.</summary>
|
||||
public IReadOnlyList<SiteInfo> TestGlobalSites => GlobalSites;
|
||||
}
|
||||
|
||||
// ── Reset messenger between tests to avoid cross-test contamination ──────
|
||||
|
||||
public GlobalSiteSelectionTests()
|
||||
{
|
||||
WeakReferenceMessenger.Default.Reset();
|
||||
}
|
||||
|
||||
// ── Helper factories ─────────────────────────────────────────────────────
|
||||
|
||||
private static StorageViewModel CreateStorageViewModel()
|
||||
=> new(
|
||||
Mock.Of<IStorageService>(),
|
||||
Mock.Of<ISessionManager>(),
|
||||
NullLogger<FeatureViewModelBase>.Instance);
|
||||
|
||||
private static PermissionsViewModel CreatePermissionsViewModel()
|
||||
=> new(
|
||||
Mock.Of<IPermissionsService>(),
|
||||
Mock.Of<ISiteListService>(),
|
||||
Mock.Of<ISessionManager>(),
|
||||
NullLogger<FeatureViewModelBase>.Instance);
|
||||
|
||||
private static TransferViewModel CreateTransferViewModel()
|
||||
=> new(
|
||||
Mock.Of<IFileTransferService>(),
|
||||
Mock.Of<ISessionManager>(),
|
||||
new BulkResultCsvExportService(),
|
||||
NullLogger<FeatureViewModelBase>.Instance);
|
||||
|
||||
private static MainWindowViewModel CreateMainWindowViewModel()
|
||||
{
|
||||
var tempFile = Path.GetTempFileName();
|
||||
var profileRepo = new ProfileRepository(tempFile);
|
||||
var profileService = new ProfileService(profileRepo);
|
||||
var sessionManager = new SessionManager(new MsalClientFactory());
|
||||
return new MainWindowViewModel(
|
||||
profileService,
|
||||
sessionManager,
|
||||
NullLogger<MainWindowViewModel>.Instance);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SiteInfo> TwoSites() =>
|
||||
new List<SiteInfo>
|
||||
{
|
||||
new("https://contoso.sharepoint.com/sites/hr", "HR"),
|
||||
new("https://contoso.sharepoint.com/sites/finance", "Finance")
|
||||
}.AsReadOnly();
|
||||
|
||||
// ── Test 1: GlobalSitesChangedMessage carries site list ──────────────────
|
||||
|
||||
[Fact]
|
||||
public void GlobalSitesChangedMessage_WhenSent_ReceiverGetsSites()
|
||||
{
|
||||
// Arrange
|
||||
IReadOnlyList<SiteInfo>? received = null;
|
||||
WeakReferenceMessenger.Default.Register<GlobalSitesChangedMessage>(
|
||||
this, (_, m) => received = m.Value);
|
||||
var sites = TwoSites();
|
||||
|
||||
// Act
|
||||
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(received);
|
||||
Assert.Equal(2, received!.Count);
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/hr", received[0].Url);
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/finance", received[1].Url);
|
||||
}
|
||||
|
||||
// ── Test 2: FeatureViewModelBase updates GlobalSites on message receive ──
|
||||
|
||||
[Fact]
|
||||
public void FeatureViewModelBase_OnGlobalSitesChangedMessage_UpdatesGlobalSitesProperty()
|
||||
{
|
||||
// Arrange
|
||||
var vm = new TestFeatureViewModel(NullLogger<FeatureViewModelBase>.Instance);
|
||||
var sites = TwoSites();
|
||||
|
||||
// Act
|
||||
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, vm.TestGlobalSites.Count);
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.TestGlobalSites[0].Url);
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/finance", vm.TestGlobalSites[1].Url);
|
||||
}
|
||||
|
||||
// ── Test 3: StorageViewModel pre-fills SiteUrl from first global site ────
|
||||
|
||||
[Fact]
|
||||
public void OnGlobalSitesChanged_WithSites_PreFillsSiteUrlOnStorageTab()
|
||||
{
|
||||
// Arrange
|
||||
var vm = CreateStorageViewModel();
|
||||
var sites = TwoSites();
|
||||
|
||||
// Act
|
||||
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SiteUrl);
|
||||
}
|
||||
|
||||
// ── Test 4: StorageViewModel local override prevents global update ────────
|
||||
|
||||
[Fact]
|
||||
public void OnGlobalSitesChanged_AfterLocalSiteEntry_DoesNotOverrideSiteUrl()
|
||||
{
|
||||
// Arrange
|
||||
var vm = CreateStorageViewModel();
|
||||
// User types a local URL — this sets the local override flag
|
||||
vm.SiteUrl = "https://contoso.sharepoint.com/sites/custom";
|
||||
|
||||
// Act: global sites message arrives
|
||||
var sites = TwoSites();
|
||||
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites));
|
||||
|
||||
// Assert: local override takes precedence
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/custom", vm.SiteUrl);
|
||||
}
|
||||
|
||||
// ── Test 5: StorageViewModel clearing SiteUrl reverts to global ──────────
|
||||
|
||||
[Fact]
|
||||
public void OnSiteUrlChanged_WhenClearedAfterLocalOverride_RevertsToGlobalSite()
|
||||
{
|
||||
// Arrange: establish global sites first, then set a local override
|
||||
var vm = CreateStorageViewModel();
|
||||
var sites = TwoSites();
|
||||
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites));
|
||||
// Confirm global was applied
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SiteUrl);
|
||||
|
||||
// User types their own URL — sets local override
|
||||
vm.SiteUrl = "https://contoso.sharepoint.com/sites/custom";
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/custom", vm.SiteUrl);
|
||||
|
||||
// Act: user clears the SiteUrl field
|
||||
vm.SiteUrl = string.Empty;
|
||||
|
||||
// Assert: override is cleared and global site is re-applied
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SiteUrl);
|
||||
}
|
||||
|
||||
// ── Test 6: PermissionsViewModel pre-populates SelectedSites from global ─
|
||||
|
||||
[Fact]
|
||||
public void OnGlobalSitesChanged_WithSites_PrePopulatesSelectedSitesOnPermissionsTab()
|
||||
{
|
||||
// Arrange
|
||||
var vm = CreatePermissionsViewModel();
|
||||
var sites = TwoSites();
|
||||
|
||||
// Act
|
||||
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, vm.SelectedSites.Count);
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SelectedSites[0].Url);
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/finance", vm.SelectedSites[1].Url);
|
||||
}
|
||||
|
||||
// ── Test 7: PermissionsViewModel local picker override prevents global ───
|
||||
|
||||
[Fact]
|
||||
public void OnGlobalSitesChanged_AfterLocalPickerOverride_DoesNotChangeSelectedSites()
|
||||
{
|
||||
// Arrange: add a site locally to simulate local site picker usage
|
||||
var vm = CreatePermissionsViewModel();
|
||||
|
||||
// Simulate what ExecuteOpenSitePicker does when user picks sites locally:
|
||||
// set _hasLocalSiteOverride = true (via reflection since it's private) and add a site.
|
||||
// We do this by using the TenantSwitchedMessage pattern to first let global sites
|
||||
// populate, then simulate a local override by directly reflecting the flag.
|
||||
var globalSites = TwoSites();
|
||||
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(globalSites));
|
||||
// Global applied — SelectedSites has 2 entries
|
||||
Assert.Equal(2, vm.SelectedSites.Count);
|
||||
|
||||
// Simulate local override: clear and add a local site, then set the override flag
|
||||
var localSite = new SiteInfo("https://contoso.sharepoint.com/sites/local", "Local");
|
||||
vm.SelectedSites.Clear();
|
||||
vm.SelectedSites.Add(localSite);
|
||||
|
||||
// Use reflection to set the private _hasLocalSiteOverride flag (same as site picker would)
|
||||
var field = typeof(PermissionsViewModel)
|
||||
.GetField("_hasLocalSiteOverride", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
field!.SetValue(vm, true);
|
||||
|
||||
// Act: new global sites arrive
|
||||
var newGlobalSites = new List<SiteInfo>
|
||||
{
|
||||
new("https://contoso.sharepoint.com/sites/new1", "New1")
|
||||
}.AsReadOnly();
|
||||
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(newGlobalSites));
|
||||
|
||||
// Assert: local override prevents global from replacing SelectedSites
|
||||
Assert.Single(vm.SelectedSites);
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/local", vm.SelectedSites[0].Url);
|
||||
}
|
||||
|
||||
// ── Test 8: Tenant switch resets local override; new global sites are applied ─
|
||||
|
||||
[Fact]
|
||||
public void TenantSwitched_AfterLocalOverride_ResetsOverrideSoNewGlobalSitesAreApplied()
|
||||
{
|
||||
// Arrange: user has a local override (different from global)
|
||||
var vm = CreateStorageViewModel();
|
||||
var sites = TwoSites();
|
||||
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites));
|
||||
// Confirm global was applied
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SiteUrl);
|
||||
|
||||
// User types a different URL — sets local override
|
||||
vm.SiteUrl = "https://contoso.sharepoint.com/sites/custom";
|
||||
|
||||
// New global sites message should NOT change SiteUrl because of local override
|
||||
var updatedGlobal = new List<SiteInfo>
|
||||
{
|
||||
new("https://contoso.sharepoint.com/sites/marketing", "Marketing")
|
||||
}.AsReadOnly();
|
||||
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(updatedGlobal));
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/custom", vm.SiteUrl); // override still active
|
||||
|
||||
// Act: tenant switch clears the override
|
||||
var newProfile = new TenantProfile
|
||||
{
|
||||
Name = "NewTenant",
|
||||
TenantUrl = "https://newtenant.sharepoint.com",
|
||||
ClientId = "new-client-id"
|
||||
};
|
||||
WeakReferenceMessenger.Default.Send(new TenantSwitchedMessage(newProfile));
|
||||
|
||||
// Send new global sites after tenant switch — override should be gone
|
||||
var newSites = new List<SiteInfo>
|
||||
{
|
||||
new("https://newtenant.sharepoint.com/sites/sales", "Sales")
|
||||
}.AsReadOnly();
|
||||
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(newSites));
|
||||
|
||||
// Assert: new global site is applied (override was reset by tenant switch)
|
||||
Assert.Equal("https://newtenant.sharepoint.com/sites/sales", vm.SiteUrl);
|
||||
}
|
||||
|
||||
// ── Test 9: TransferViewModel pre-fills SourceSiteUrl from first global ──
|
||||
|
||||
[Fact]
|
||||
public void OnGlobalSitesChanged_WithSites_PreFillsSourceSiteUrlOnTransferTab()
|
||||
{
|
||||
// Arrange
|
||||
var vm = CreateTransferViewModel();
|
||||
var sites = TwoSites();
|
||||
|
||||
// Act
|
||||
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites));
|
||||
|
||||
// Assert: only SourceSiteUrl is pre-filled (first global site)
|
||||
Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SourceSiteUrl);
|
||||
}
|
||||
|
||||
// ── Test 10: MainWindowViewModel GlobalSitesSelectedLabel updates with count
|
||||
|
||||
[Fact]
|
||||
public void GlobalSitesSelectedLabel_WhenSitesAdded_ReflectsCount()
|
||||
{
|
||||
// Arrange
|
||||
var vm = CreateMainWindowViewModel();
|
||||
// Initially no sites selected
|
||||
var initialLabel = vm.GlobalSitesSelectedLabel;
|
||||
Assert.DoesNotContain("1", initialLabel); // Should say "none" equivalent
|
||||
|
||||
// Act: add two sites
|
||||
vm.GlobalSelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com/sites/hr", "HR"));
|
||||
vm.GlobalSelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com/sites/finance", "Finance"));
|
||||
|
||||
// Assert: label reflects the count
|
||||
var label = vm.GlobalSitesSelectedLabel;
|
||||
Assert.Contains("2", label);
|
||||
// Ensure label is non-empty (different from the initial "none" state)
|
||||
Assert.NotEqual(initialLabel, label);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user