using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.Messaging; using Microsoft.Extensions.Logging.Abstractions; using Moq; using SharepointToolbox.Core.Models; using SharepointToolbox.Services; using SharepointToolbox.ViewModels; using SharepointToolbox.ViewModels.Tabs; namespace SharepointToolbox.Tests.ViewModels; /// /// Unit tests for directory browse mode in UserAccessAuditViewModel (Phase 13 Plan 02). /// Verifies: directory load, progress, cancellation, member/guest filter, text filter, /// sorting, tenant switch reset, and no regression on search mode. /// [Trait("Category", "Unit")] public class UserAccessAuditViewModelDirectoryTests { public UserAccessAuditViewModelDirectoryTests() { WeakReferenceMessenger.Default.Reset(); } // ── Helper factories ────────────────────────────────────────────────────── private static GraphDirectoryUser MakeMember(string name = "Alice", string dept = "IT", string jobTitle = "Engineer") => new(name, $"{name.ToLower().Replace(" ", "")}@contoso.com", null, dept, jobTitle, "Member"); private static GraphDirectoryUser MakeGuest(string name = "Bob External") => new(name, $"{name.ToLower().Replace(" ", "")}@external.com", null, null, null, "Guest"); private static (UserAccessAuditViewModel vm, Mock dirMock, Mock auditMock) CreateViewModel(IReadOnlyList? directoryResult = null) { var mockAudit = new Mock(); var mockGraph = new Mock(); var mockSession = new Mock(); var mockDir = new Mock(); mockDir.Setup(s => s.GetUsersAsync( It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync(directoryResult ?? Array.Empty()); var vm = new UserAccessAuditViewModel( mockAudit.Object, mockGraph.Object, mockSession.Object, NullLogger.Instance, graphUserDirectoryService: mockDir.Object); vm._currentProfile = new TenantProfile { Name = "Test", TenantUrl = "https://contoso.sharepoint.com", ClientId = "test-client-id" }; return (vm, mockDir, mockAudit); } // ── Test 1: IsBrowseMode defaults to false ─────────────────────────────── [Fact] public void IsBrowseMode_defaults_to_false() { var (vm, _, _) = CreateViewModel(); Assert.False(vm.IsBrowseMode); } // ── Test 2: DirectoryUsers is empty by default ─────────────────────────── [Fact] public void DirectoryUsers_empty_by_default() { var (vm, _, _) = CreateViewModel(); Assert.Empty(vm.DirectoryUsers); } // ── Test 3: Commands are not null ───────────────────────────────────────── [Fact] public void LoadDirectoryCommand_and_CancelDirectoryLoadCommand_not_null() { var (vm, _, _) = CreateViewModel(); Assert.NotNull(vm.LoadDirectoryCommand); Assert.NotNull(vm.CancelDirectoryLoadCommand); } // ── Test 4: LoadDirectoryAsync populates DirectoryUsers ────────────────── [Fact] public async Task LoadDirectoryAsync_populates_DirectoryUsers() { var users = new List { MakeMember("Alice"), MakeMember("Charlie") }; var (vm, _, _) = CreateViewModel(users); await vm.TestLoadDirectoryAsync(); Assert.Equal(2, vm.DirectoryUsers.Count); Assert.Contains(vm.DirectoryUsers, u => u.DisplayName == "Alice"); Assert.Contains(vm.DirectoryUsers, u => u.DisplayName == "Charlie"); } // ── Test 5: LoadDirectoryAsync reports progress via DirectoryLoadStatus ── [Fact] public async Task LoadDirectoryAsync_sets_DirectoryLoadStatus_on_completion() { var users = new List { MakeMember("Alice") }; var (vm, _, _) = CreateViewModel(users); await vm.TestLoadDirectoryAsync(); Assert.Equal("1 users loaded", vm.DirectoryLoadStatus); } // ── Test 6: LoadDirectoryAsync with no profile sets StatusMessage ───────── [Fact] public async Task LoadDirectoryAsync_with_no_profile_sets_StatusMessage() { var (vm, _, _) = CreateViewModel(); vm._currentProfile = null; await vm.TestLoadDirectoryAsync(); Assert.Equal("No tenant profile selected. Please connect first.", vm.StatusMessage); Assert.Empty(vm.DirectoryUsers); } // ── Test 7: CancelDirectoryLoadCommand cancels in-flight load ──────────── [Fact] public async Task CancelDirectoryLoad_cancels_inflight_load() { var tcs = new TaskCompletionSource>(); var mockDir = new Mock(); mockDir.Setup(s => s.GetUsersAsync( It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) .Returns?, CancellationToken>((_, _, _, ct) => { var localTcs = new TaskCompletionSource>(); ct.Register(() => localTcs.TrySetCanceled(ct)); return localTcs.Task; }); var vm = new UserAccessAuditViewModel( new Mock().Object, new Mock().Object, new Mock().Object, NullLogger.Instance, graphUserDirectoryService: mockDir.Object); vm._currentProfile = new TenantProfile { Name = "Test", TenantUrl = "https://contoso.sharepoint.com", ClientId = "test-client-id" }; // Start load (will block on the mock) var loadTask = vm.TestLoadDirectoryAsync(); // Cancel vm.CancelDirectoryLoadCommand.Execute(null); await loadTask; Assert.Equal("Load cancelled.", vm.DirectoryLoadStatus); Assert.False(vm.IsLoadingDirectory); } // ── Test 8: IncludeGuests=false filters out Guest users ────────────────── [Fact] public void IncludeGuests_false_filters_out_guest_users() { var (vm, _, _) = CreateViewModel(); vm.DirectoryUsers.Add(MakeMember("Alice")); vm.DirectoryUsers.Add(MakeGuest("Bob External")); vm.DirectoryUsers.Add(MakeMember("Charlie")); vm.IncludeGuests = false; var visible = vm.DirectoryUsersView.Cast().ToList(); Assert.Equal(2, visible.Count); Assert.All(visible, u => Assert.Equal("Member", u.UserType)); } // ── Test 9: IncludeGuests=true shows all users ─────────────────────────── [Fact] public void IncludeGuests_true_shows_all_users() { var (vm, _, _) = CreateViewModel(); vm.DirectoryUsers.Add(MakeMember("Alice")); vm.DirectoryUsers.Add(MakeGuest("Bob External")); vm.IncludeGuests = true; var visible = vm.DirectoryUsersView.Cast().ToList(); Assert.Equal(2, visible.Count); } // ── Test 10: DirectoryFilterText filters by DisplayName ────────────────── [Fact] public void DirectoryFilterText_filters_by_DisplayName() { var (vm, _, _) = CreateViewModel(); vm.DirectoryUsers.Add(MakeMember("Alice")); vm.DirectoryUsers.Add(MakeMember("Charlie")); vm.IncludeGuests = true; vm.DirectoryFilterText = "Ali"; var visible = vm.DirectoryUsersView.Cast().ToList(); Assert.Single(visible); Assert.Equal("Alice", visible[0].DisplayName); } // ── Test 11: DirectoryFilterText filters by Department ─────────────────── [Fact] public void DirectoryFilterText_filters_by_Department() { var (vm, _, _) = CreateViewModel(); vm.DirectoryUsers.Add(MakeMember("Alice", dept: "Engineering")); vm.DirectoryUsers.Add(MakeMember("Charlie", dept: "Marketing")); vm.IncludeGuests = true; vm.DirectoryFilterText = "Market"; var visible = vm.DirectoryUsersView.Cast().ToList(); Assert.Single(visible); Assert.Equal("Charlie", visible[0].DisplayName); } // ── Test 12: DirectoryUsersView default sort is DisplayName ascending ──── [Fact] public void DirectoryUsersView_sorted_by_DisplayName_ascending() { var (vm, _, _) = CreateViewModel(); vm.DirectoryUsers.Add(MakeMember("Charlie")); vm.DirectoryUsers.Add(MakeMember("Alice")); vm.DirectoryUsers.Add(MakeMember("Bob")); vm.IncludeGuests = true; var visible = vm.DirectoryUsersView.Cast().ToList(); Assert.Equal("Alice", visible[0].DisplayName); Assert.Equal("Bob", visible[1].DisplayName); Assert.Equal("Charlie", visible[2].DisplayName); } // ── Test 13: OnTenantSwitched clears directory state ───────────────────── [Fact] public async Task OnTenantSwitched_clears_directory_state() { var users = new List { MakeMember("Alice") }; var (vm, _, _) = CreateViewModel(users); // Load directory await vm.TestLoadDirectoryAsync(); Assert.NotEmpty(vm.DirectoryUsers); vm.IsBrowseMode = true; vm.DirectoryFilterText = "test"; vm.IncludeGuests = true; // Act: switch tenant var newProfile = new TenantProfile { Name = "NewTenant", TenantUrl = "https://newtenant.sharepoint.com", ClientId = "new-client-id" }; WeakReferenceMessenger.Default.Send(new Core.Messages.TenantSwitchedMessage(newProfile)); // Assert Assert.Empty(vm.DirectoryUsers); Assert.False(vm.IsBrowseMode); Assert.Empty(vm.DirectoryFilterText); Assert.Empty(vm.DirectoryLoadStatus); Assert.False(vm.IsLoadingDirectory); Assert.False(vm.IncludeGuests); } // ── Test 14: DirectoryUserCount reflects filtered count ─────────────────── [Fact] public void DirectoryUserCount_reflects_filtered_count() { var (vm, _, _) = CreateViewModel(); vm.DirectoryUsers.Add(MakeMember("Alice")); vm.DirectoryUsers.Add(MakeGuest("Bob External")); vm.DirectoryUsers.Add(MakeMember("Charlie")); // With guests hidden (default IncludeGuests=false) vm.IncludeGuests = false; Assert.Equal(2, vm.DirectoryUserCount); // With guests shown vm.IncludeGuests = true; Assert.Equal(3, vm.DirectoryUserCount); // With text filter vm.DirectoryFilterText = "Ali"; Assert.Equal(1, vm.DirectoryUserCount); } // ── Test 15: Search mode still works (no regression) ───────────────────── [Fact] public void Search_mode_SelectedUsers_still_works() { var (vm, _, _) = CreateViewModel(); // Search mode properties should be functional Assert.Empty(vm.SelectedUsers); vm.SelectedUsers.Add(new GraphUserResult("Alice Smith", "alice@contoso.com", "alice@contoso.com")); Assert.Single(vm.SelectedUsers); Assert.Equal("1 user(s) selected", vm.SelectedUsersLabel); } // ── Test 16: DirectoryFilterText filters by JobTitle ───────────────────── [Fact] public void DirectoryFilterText_filters_by_JobTitle() { var (vm, _, _) = CreateViewModel(); vm.DirectoryUsers.Add(MakeMember("Alice", jobTitle: "Senior Developer")); vm.DirectoryUsers.Add(MakeMember("Charlie", jobTitle: "Product Manager")); vm.IncludeGuests = true; vm.DirectoryFilterText = "Developer"; var visible = vm.DirectoryUsersView.Cast().ToList(); Assert.Single(visible); Assert.Equal("Alice", visible[0].DisplayName); } // ── Test 17: SelectDirectoryUserCommand adds user to SelectedUsers ────── [Fact] public void SelectDirectoryUserCommand_adds_user_to_SelectedUsers() { var (vm, _, _) = CreateViewModel(); var dirUser = MakeMember("Alice"); vm.SelectDirectoryUserCommand.Execute(dirUser); Assert.Single(vm.SelectedUsers); Assert.Equal("Alice", vm.SelectedUsers[0].DisplayName); Assert.Equal("alice@contoso.com", vm.SelectedUsers[0].UserPrincipalName); } // ── Test 18: SelectDirectoryUserCommand skips duplicates ───────────────── [Fact] public void SelectDirectoryUserCommand_skips_duplicates() { var (vm, _, _) = CreateViewModel(); var dirUser = MakeMember("Alice"); vm.SelectDirectoryUserCommand.Execute(dirUser); vm.SelectDirectoryUserCommand.Execute(dirUser); Assert.Single(vm.SelectedUsers); } // ── Test 19: SelectDirectoryUserCommand with null does nothing ─────────── [Fact] public void SelectDirectoryUserCommand_with_null_does_nothing() { var (vm, _, _) = CreateViewModel(); vm.SelectDirectoryUserCommand.Execute(null); Assert.Empty(vm.SelectedUsers); } // ── Test 20: After SelectDirectoryUser, user can be audited ────────────── [Fact] public void SelectDirectoryUser_adds_auditable_user_to_SelectedUsers() { var (vm, _, _) = CreateViewModel(); var dirUser = MakeMember("Alice"); vm.SelectDirectoryUserCommand.Execute(dirUser); Assert.True(vm.SelectedUsers.Count > 0); Assert.Equal("alice@contoso.com", vm.SelectedUsers[0].UserPrincipalName); } }