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);
}
}