From 80ef092a2ea4d9d77d9c7609f479999533d7103c Mon Sep 17 00:00:00 2001 From: Dev Date: Tue, 7 Apr 2026 10:13:31 +0200 Subject: [PATCH] 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 --- .../ViewModels/GlobalSiteSelectionTests.cs | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs diff --git a/SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs b/SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs new file mode 100644 index 0000000..c1d8429 --- /dev/null +++ b/SharepointToolbox.Tests/ViewModels/GlobalSiteSelectionTests.cs @@ -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; + +/// +/// 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 +/// +public class GlobalSiteSelectionTests +{ + // ── Helper: minimal concrete subclass of FeatureViewModelBase ──────────── + + private class TestFeatureViewModel : FeatureViewModelBase + { + public TestFeatureViewModel(ILogger logger) : base(logger) { } + + protected override Task RunOperationAsync(CancellationToken ct, IProgress progress) + => Task.CompletedTask; + + /// Expose protected GlobalSites for assertions. + public IReadOnlyList 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(), + Mock.Of(), + NullLogger.Instance); + + private static PermissionsViewModel CreatePermissionsViewModel() + => new( + Mock.Of(), + Mock.Of(), + Mock.Of(), + NullLogger.Instance); + + private static TransferViewModel CreateTransferViewModel() + => new( + Mock.Of(), + Mock.Of(), + new BulkResultCsvExportService(), + NullLogger.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.Instance); + } + + private static IReadOnlyList TwoSites() => + new List + { + 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? received = null; + WeakReferenceMessenger.Default.Register( + 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.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 + { + 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 + { + 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 + { + 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); + } +}