From 67a2053a94321221e4a0d02ba70a2c44ccf91d29 Mon Sep 17 00:00:00 2001 From: Dev Date: Tue, 7 Apr 2026 13:15:16 +0200 Subject: [PATCH] test(07-10): add debounced search unit test for UserAccessAuditViewModel - Extended CreateViewModel helper to return (vm, auditMock, graphMock) 3-tuple - Updated all 8 existing tests to use _ discard for the new graphMock slot - Added Test 9: SearchQuery_debounced_calls_SearchUsersAsync verifying that setting SearchQuery to "Ali" calls SearchUsersAsync after 300ms debounce - All 9 ViewModel tests pass; full suite 177 passed / 22 skipped --- .../UserAccessAuditViewModelTests.cs | 67 ++++++++++++++++--- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs b/SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs index d105a01..f3bdcad 100644 --- a/SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs +++ b/SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs @@ -41,8 +41,8 @@ public class UserAccessAuditViewModelTests string upn = "alice@contoso.com") => new(display, upn, upn); - /// Creates a ViewModel wired with a mock IUserAccessAuditService. - private static (UserAccessAuditViewModel vm, Mock auditMock) + /// Creates a ViewModel wired with mock services. + private static (UserAccessAuditViewModel vm, Mock auditMock, Mock graphMock) CreateViewModel(IReadOnlyList? auditResult = null) { var mockAudit = new Mock(); @@ -65,7 +65,7 @@ public class UserAccessAuditViewModelTests mockSession.Object, NullLogger.Instance); - return (vm, mockAudit); + return (vm, mockAudit, mockGraph); } // ── Test 1: RunOperation calls AuditUsersAsync ──────────────────────────── @@ -73,7 +73,7 @@ public class UserAccessAuditViewModelTests [Fact] public async Task RunOperation_calls_AuditUsersAsync() { - var (vm, auditMock) = CreateViewModel(); + var (vm, auditMock, _) = CreateViewModel(); vm.SelectedUsers.Add(MakeUser()); vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso")); @@ -102,7 +102,7 @@ public class UserAccessAuditViewModelTests MakeEntry(userLogin: "bob@contoso.com") }; - var (vm, _) = CreateViewModel(entries); + var (vm, _, _) = CreateViewModel(entries); vm.SelectedUsers.Add(MakeUser()); vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso")); @@ -124,7 +124,7 @@ public class UserAccessAuditViewModelTests MakeEntry(userLogin: "bob@contoso.com", siteUrl: "https://contoso.sharepoint.com/sites/s1", isHighPrivilege: true) }; - var (vm, _) = CreateViewModel(entries); + var (vm, _, _) = CreateViewModel(entries); vm.SelectedUsers.Add(MakeUser()); vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso")); @@ -142,7 +142,7 @@ public class UserAccessAuditViewModelTests public async Task OnTenantSwitched_resets_state() { var entries = new List { MakeEntry() }; - var (vm, _) = CreateViewModel(entries); + var (vm, _, _) = CreateViewModel(entries); // Populate state vm.SelectedUsers.Add(MakeUser()); @@ -172,7 +172,7 @@ public class UserAccessAuditViewModelTests [Fact] public void OnGlobalSitesChanged_updates_selected_sites() { - var (vm, _) = CreateViewModel(); + var (vm, _, _) = CreateViewModel(); var sites = new List { new("https://contoso.sharepoint.com/sites/hr", "HR"), @@ -190,7 +190,7 @@ public class UserAccessAuditViewModelTests [Fact] public void OnGlobalSitesChanged_skipped_when_override() { - var (vm, _) = CreateViewModel(); + var (vm, _, _) = CreateViewModel(); // Add a local site and set the override flag via reflection var localSite = new SiteInfo("https://contoso.sharepoint.com/sites/local", "Local"); @@ -218,7 +218,7 @@ public class UserAccessAuditViewModelTests [Fact] public void CanExport_false_when_no_results() { - var (vm, _) = CreateViewModel(); + var (vm, _, _) = CreateViewModel(); // Results is empty by default Assert.Empty(vm.Results); @@ -232,7 +232,7 @@ public class UserAccessAuditViewModelTests public async Task CanExport_true_when_has_results() { var entries = new List { MakeEntry() }; - var (vm, _) = CreateViewModel(entries); + var (vm, _, _) = CreateViewModel(entries); vm.SelectedUsers.Add(MakeUser()); vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso")); @@ -243,4 +243,49 @@ public class UserAccessAuditViewModelTests Assert.True(vm.ExportCsvCommand.CanExecute(null)); Assert.True(vm.ExportHtmlCommand.CanExecute(null)); } + + // ── Test 9: Debounced search triggers SearchUsersAsync ─────────────────── + + [Fact] + public async Task SearchQuery_debounced_calls_SearchUsersAsync() + { + var graphResults = new List + { + new("Alice Smith", "alice@contoso.com", "alice@contoso.com") + }; + + var (vm, _, graphMock) = CreateViewModel(); + + graphMock + .Setup(s => s.SearchUsersAsync( + It.IsAny(), + It.Is(q => q == "Ali"), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(graphResults); + + // Set a TenantProfile so _currentProfile is non-null + var profile = new TenantProfile + { + Name = "Test", + TenantUrl = "https://contoso.sharepoint.com", + ClientId = "test-client-id" + }; + WeakReferenceMessenger.Default.Send(new TenantSwitchedMessage(profile)); + + // Act: set SearchQuery which triggers OnSearchQueryChanged → DebounceSearchAsync + vm.SearchQuery = "Ali"; + + // Wait longer than 300ms debounce to allow async fire-and-forget to complete + await Task.Delay(600); + + // Assert: SearchUsersAsync was called with the query + graphMock.Verify( + s => s.SearchUsersAsync( + It.IsAny(), + "Ali", + It.IsAny(), + It.IsAny()), + Times.Once); + } }