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
This commit is contained in:
Dev
2026-04-07 13:15:16 +02:00
parent 33833dce5d
commit 67a2053a94

View File

@@ -41,8 +41,8 @@ public class UserAccessAuditViewModelTests
string upn = "alice@contoso.com") => string upn = "alice@contoso.com") =>
new(display, upn, upn); new(display, upn, upn);
/// <summary>Creates a ViewModel wired with a mock IUserAccessAuditService.</summary> /// <summary>Creates a ViewModel wired with mock services.</summary>
private static (UserAccessAuditViewModel vm, Mock<IUserAccessAuditService> auditMock) private static (UserAccessAuditViewModel vm, Mock<IUserAccessAuditService> auditMock, Mock<IGraphUserSearchService> graphMock)
CreateViewModel(IReadOnlyList<UserAccessEntry>? auditResult = null) CreateViewModel(IReadOnlyList<UserAccessEntry>? auditResult = null)
{ {
var mockAudit = new Mock<IUserAccessAuditService>(); var mockAudit = new Mock<IUserAccessAuditService>();
@@ -65,7 +65,7 @@ public class UserAccessAuditViewModelTests
mockSession.Object, mockSession.Object,
NullLogger<FeatureViewModelBase>.Instance); NullLogger<FeatureViewModelBase>.Instance);
return (vm, mockAudit); return (vm, mockAudit, mockGraph);
} }
// ── Test 1: RunOperation calls AuditUsersAsync ──────────────────────────── // ── Test 1: RunOperation calls AuditUsersAsync ────────────────────────────
@@ -73,7 +73,7 @@ public class UserAccessAuditViewModelTests
[Fact] [Fact]
public async Task RunOperation_calls_AuditUsersAsync() public async Task RunOperation_calls_AuditUsersAsync()
{ {
var (vm, auditMock) = CreateViewModel(); var (vm, auditMock, _) = CreateViewModel();
vm.SelectedUsers.Add(MakeUser()); vm.SelectedUsers.Add(MakeUser());
vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso")); vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso"));
@@ -102,7 +102,7 @@ public class UserAccessAuditViewModelTests
MakeEntry(userLogin: "bob@contoso.com") MakeEntry(userLogin: "bob@contoso.com")
}; };
var (vm, _) = CreateViewModel(entries); var (vm, _, _) = CreateViewModel(entries);
vm.SelectedUsers.Add(MakeUser()); vm.SelectedUsers.Add(MakeUser());
vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso")); 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) 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.SelectedUsers.Add(MakeUser());
vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso")); vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso"));
@@ -142,7 +142,7 @@ public class UserAccessAuditViewModelTests
public async Task OnTenantSwitched_resets_state() public async Task OnTenantSwitched_resets_state()
{ {
var entries = new List<UserAccessEntry> { MakeEntry() }; var entries = new List<UserAccessEntry> { MakeEntry() };
var (vm, _) = CreateViewModel(entries); var (vm, _, _) = CreateViewModel(entries);
// Populate state // Populate state
vm.SelectedUsers.Add(MakeUser()); vm.SelectedUsers.Add(MakeUser());
@@ -172,7 +172,7 @@ public class UserAccessAuditViewModelTests
[Fact] [Fact]
public void OnGlobalSitesChanged_updates_selected_sites() public void OnGlobalSitesChanged_updates_selected_sites()
{ {
var (vm, _) = CreateViewModel(); var (vm, _, _) = CreateViewModel();
var sites = new List<SiteInfo> var sites = new List<SiteInfo>
{ {
new("https://contoso.sharepoint.com/sites/hr", "HR"), new("https://contoso.sharepoint.com/sites/hr", "HR"),
@@ -190,7 +190,7 @@ public class UserAccessAuditViewModelTests
[Fact] [Fact]
public void OnGlobalSitesChanged_skipped_when_override() public void OnGlobalSitesChanged_skipped_when_override()
{ {
var (vm, _) = CreateViewModel(); var (vm, _, _) = CreateViewModel();
// Add a local site and set the override flag via reflection // Add a local site and set the override flag via reflection
var localSite = new SiteInfo("https://contoso.sharepoint.com/sites/local", "Local"); var localSite = new SiteInfo("https://contoso.sharepoint.com/sites/local", "Local");
@@ -218,7 +218,7 @@ public class UserAccessAuditViewModelTests
[Fact] [Fact]
public void CanExport_false_when_no_results() public void CanExport_false_when_no_results()
{ {
var (vm, _) = CreateViewModel(); var (vm, _, _) = CreateViewModel();
// Results is empty by default // Results is empty by default
Assert.Empty(vm.Results); Assert.Empty(vm.Results);
@@ -232,7 +232,7 @@ public class UserAccessAuditViewModelTests
public async Task CanExport_true_when_has_results() public async Task CanExport_true_when_has_results()
{ {
var entries = new List<UserAccessEntry> { MakeEntry() }; var entries = new List<UserAccessEntry> { MakeEntry() };
var (vm, _) = CreateViewModel(entries); var (vm, _, _) = CreateViewModel(entries);
vm.SelectedUsers.Add(MakeUser()); vm.SelectedUsers.Add(MakeUser());
vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso")); 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.ExportCsvCommand.CanExecute(null));
Assert.True(vm.ExportHtmlCommand.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<GraphUserResult>
{
new("Alice Smith", "alice@contoso.com", "alice@contoso.com")
};
var (vm, _, graphMock) = CreateViewModel();
graphMock
.Setup(s => s.SearchUsersAsync(
It.IsAny<string>(),
It.Is<string>(q => q == "Ali"),
It.IsAny<int>(),
It.IsAny<CancellationToken>()))
.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<string>(),
"Ali",
It.IsAny<int>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
} }