test(07-08): add export and ViewModel unit tests
- UserAccessCsvExportServiceTests (5): summary section, data header, RFC 4180 quote escaping, 7-column count, WriteSingleFileAsync multi-user output - UserAccessHtmlExportServiceTests (7): DOCTYPE, stats cards, dual-view sections, access type badges, filterTable JS, toggleView JS, HTML entity encoding - UserAccessAuditViewModelTests (8): AuditUsersAsync invocation, results population, summary properties computation, tenant switch reset, GlobalSitesChanged update, override guard, CanExport false/true states
This commit is contained in:
@@ -0,0 +1,158 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using SharepointToolbox.Core.Models;
|
||||||
|
using SharepointToolbox.Services.Export;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.Tests.Services.Export;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unit tests for UserAccessCsvExportService (Phase 7 Plan 08).
|
||||||
|
/// Verifies: summary section, column count, RFC 4180 escaping, per-user content.
|
||||||
|
/// </summary>
|
||||||
|
public class UserAccessCsvExportServiceTests
|
||||||
|
{
|
||||||
|
// ── Helper factory ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static UserAccessEntry MakeEntry(
|
||||||
|
string userDisplay = "Alice Smith",
|
||||||
|
string userLogin = "alice@contoso.com",
|
||||||
|
string siteUrl = "https://contoso.sharepoint.com",
|
||||||
|
string siteTitle = "Contoso",
|
||||||
|
string objectType = "List",
|
||||||
|
string objectTitle = "Docs",
|
||||||
|
string objectUrl = "https://contoso.sharepoint.com/Docs",
|
||||||
|
string permLevel = "Read",
|
||||||
|
AccessType accessType = AccessType.Direct,
|
||||||
|
string grantedThrough = "Direct Permissions",
|
||||||
|
bool isHighPrivilege = false,
|
||||||
|
bool isExternal = false) =>
|
||||||
|
new(userDisplay, userLogin, siteUrl, siteTitle, objectType, objectTitle, objectUrl,
|
||||||
|
permLevel, accessType, grantedThrough, isHighPrivilege, isExternal);
|
||||||
|
|
||||||
|
private static readonly UserAccessEntry DefaultEntry = MakeEntry();
|
||||||
|
|
||||||
|
// ── Test 1: BuildCsv includes summary section ─────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildCsv_includes_summary_section()
|
||||||
|
{
|
||||||
|
var svc = new UserAccessCsvExportService();
|
||||||
|
var csv = svc.BuildCsv("Alice Smith", "alice@contoso.com", new[] { DefaultEntry });
|
||||||
|
|
||||||
|
Assert.Contains("User Access Audit Report", csv);
|
||||||
|
Assert.Contains("Alice Smith", csv);
|
||||||
|
Assert.Contains("alice@contoso.com", csv);
|
||||||
|
Assert.Contains("Total Accesses", csv);
|
||||||
|
Assert.Contains("Sites", csv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 2: BuildCsv includes data header line ────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildCsv_includes_data_header()
|
||||||
|
{
|
||||||
|
var svc = new UserAccessCsvExportService();
|
||||||
|
var csv = svc.BuildCsv("Alice Smith", "alice@contoso.com", new[] { DefaultEntry });
|
||||||
|
|
||||||
|
Assert.Contains("Site", csv);
|
||||||
|
Assert.Contains("Object Type", csv);
|
||||||
|
Assert.Contains("Object", csv);
|
||||||
|
Assert.Contains("Permission Level", csv);
|
||||||
|
Assert.Contains("Access Type", csv);
|
||||||
|
Assert.Contains("Granted Through", csv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 3: BuildCsv escapes double quotes (RFC 4180) ─────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildCsv_escapes_quotes()
|
||||||
|
{
|
||||||
|
var entryWithQuotes = MakeEntry(objectTitle: "Document \"Template\" Library");
|
||||||
|
var svc = new UserAccessCsvExportService();
|
||||||
|
var csv = svc.BuildCsv("Alice", "alice@contoso.com", new[] { entryWithQuotes });
|
||||||
|
|
||||||
|
// RFC 4180: double quotes inside a quoted field are doubled
|
||||||
|
Assert.Contains("\"\"Template\"\"", csv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 4: BuildCsv data rows have correct column count ──────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildCsv_correct_column_count()
|
||||||
|
{
|
||||||
|
var svc = new UserAccessCsvExportService();
|
||||||
|
var csv = svc.BuildCsv("Alice", "alice@contoso.com", new[] { DefaultEntry });
|
||||||
|
|
||||||
|
// Find the header row and count its quoted comma-separated fields
|
||||||
|
// Header is: "Site","Object Type","Object","URL","Permission Level","Access Type","Granted Through"
|
||||||
|
// That is 7 fields.
|
||||||
|
var lines = csv.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
// Find a data row (after the blank line separating summary from data)
|
||||||
|
// Data rows contain the entry content (not the header line itself)
|
||||||
|
// We want to count fields in the header row:
|
||||||
|
var headerLine = lines.FirstOrDefault(l => l.Contains("\"Site\",\"Object Type\""));
|
||||||
|
Assert.NotNull(headerLine);
|
||||||
|
|
||||||
|
// Count comma-separated quoted fields: split by "," boundary
|
||||||
|
var fields = CountCsvFields(headerLine!);
|
||||||
|
Assert.Equal(7, fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 5: WriteSingleFileAsync includes entries for all users ───────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WriteSingleFileAsync_includes_all_users()
|
||||||
|
{
|
||||||
|
var alice = MakeEntry(userDisplay: "Alice", userLogin: "alice@contoso.com");
|
||||||
|
var bob = MakeEntry(userDisplay: "Bob", userLogin: "bob@contoso.com");
|
||||||
|
|
||||||
|
var svc = new UserAccessCsvExportService();
|
||||||
|
var tmpFile = Path.GetTempFileName();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await svc.WriteSingleFileAsync(new[] { alice, bob }, tmpFile, CancellationToken.None);
|
||||||
|
var content = await File.ReadAllTextAsync(tmpFile);
|
||||||
|
|
||||||
|
Assert.Contains("alice@contoso.com", content);
|
||||||
|
Assert.Contains("bob@contoso.com", content);
|
||||||
|
Assert.Contains("Users Audited", content);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
File.Delete(tmpFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Private helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Counts the number of comma-separated fields in a CSV line by stripping
|
||||||
|
/// surrounding quotes from each field.
|
||||||
|
/// </summary>
|
||||||
|
private static int CountCsvFields(string line)
|
||||||
|
{
|
||||||
|
// Simple RFC 4180 field counter — works for well-formed quoted fields
|
||||||
|
int count = 1;
|
||||||
|
bool inQuotes = false;
|
||||||
|
for (int i = 0; i < line.Length; i++)
|
||||||
|
{
|
||||||
|
char c = line[i];
|
||||||
|
if (c == '"')
|
||||||
|
{
|
||||||
|
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
|
||||||
|
i++; // skip escaped quote
|
||||||
|
else
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
}
|
||||||
|
else if (c == ',' && !inQuotes)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using SharepointToolbox.Core.Models;
|
||||||
|
using SharepointToolbox.Services.Export;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.Tests.Services.Export;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unit tests for UserAccessHtmlExportService (Phase 7 Plan 08).
|
||||||
|
/// Verifies: DOCTYPE, stats cards, dual-view sections, access type badges,
|
||||||
|
/// filter script, toggle script, HTML entity encoding.
|
||||||
|
/// </summary>
|
||||||
|
public class UserAccessHtmlExportServiceTests
|
||||||
|
{
|
||||||
|
// ── Helper factory ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static UserAccessEntry MakeEntry(
|
||||||
|
string userDisplay = "Alice Smith",
|
||||||
|
string userLogin = "alice@contoso.com",
|
||||||
|
string siteUrl = "https://contoso.sharepoint.com",
|
||||||
|
string siteTitle = "Contoso",
|
||||||
|
string objectType = "List",
|
||||||
|
string objectTitle = "Docs",
|
||||||
|
string objectUrl = "https://contoso.sharepoint.com/Docs",
|
||||||
|
string permLevel = "Read",
|
||||||
|
AccessType accessType = AccessType.Direct,
|
||||||
|
string grantedThrough = "Direct Permissions",
|
||||||
|
bool isHighPrivilege = false,
|
||||||
|
bool isExternal = false) =>
|
||||||
|
new(userDisplay, userLogin, siteUrl, siteTitle, objectType, objectTitle, objectUrl,
|
||||||
|
permLevel, accessType, grantedThrough, isHighPrivilege, isExternal);
|
||||||
|
|
||||||
|
private static readonly UserAccessEntry DefaultEntry = MakeEntry();
|
||||||
|
|
||||||
|
// ── Test 1: BuildHtml contains DOCTYPE ───────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildHtml_contains_doctype()
|
||||||
|
{
|
||||||
|
var svc = new UserAccessHtmlExportService();
|
||||||
|
var html = svc.BuildHtml(new[] { DefaultEntry });
|
||||||
|
|
||||||
|
Assert.StartsWith("<!DOCTYPE html>", html.TrimStart());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 2: BuildHtml has stats cards ─────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildHtml_has_stats_cards()
|
||||||
|
{
|
||||||
|
var svc = new UserAccessHtmlExportService();
|
||||||
|
var html = svc.BuildHtml(new[] { DefaultEntry });
|
||||||
|
|
||||||
|
Assert.Contains("Total Accesses", html);
|
||||||
|
Assert.Contains("stat-card", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 3: BuildHtml has both view sections ──────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildHtml_has_both_views()
|
||||||
|
{
|
||||||
|
var svc = new UserAccessHtmlExportService();
|
||||||
|
var html = svc.BuildHtml(new[] { DefaultEntry });
|
||||||
|
|
||||||
|
// By-user view
|
||||||
|
Assert.Contains("view-user", html);
|
||||||
|
// By-site view
|
||||||
|
Assert.Contains("view-site", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 4: BuildHtml has access type badge CSS classes ───────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildHtml_has_access_type_badges()
|
||||||
|
{
|
||||||
|
var entries = new List<UserAccessEntry>
|
||||||
|
{
|
||||||
|
MakeEntry(accessType: AccessType.Direct),
|
||||||
|
MakeEntry(userLogin: "bob@contoso.com", accessType: AccessType.Group),
|
||||||
|
MakeEntry(userLogin: "carol@contoso.com", accessType: AccessType.Inherited)
|
||||||
|
};
|
||||||
|
|
||||||
|
var svc = new UserAccessHtmlExportService();
|
||||||
|
var html = svc.BuildHtml(entries);
|
||||||
|
|
||||||
|
Assert.Contains("access-direct", html);
|
||||||
|
Assert.Contains("access-group", html);
|
||||||
|
Assert.Contains("access-inherited", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 5: BuildHtml has filterTable JS function ─────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildHtml_has_filter_script()
|
||||||
|
{
|
||||||
|
var svc = new UserAccessHtmlExportService();
|
||||||
|
var html = svc.BuildHtml(new[] { DefaultEntry });
|
||||||
|
|
||||||
|
Assert.Contains("filterTable", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 6: BuildHtml has toggleView JS function ──────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildHtml_has_toggle_script()
|
||||||
|
{
|
||||||
|
var svc = new UserAccessHtmlExportService();
|
||||||
|
var html = svc.BuildHtml(new[] { DefaultEntry });
|
||||||
|
|
||||||
|
Assert.Contains("toggleView", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 7: BuildHtml encodes HTML entities ───────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildHtml_encodes_html_entities()
|
||||||
|
{
|
||||||
|
var entryWithScript = MakeEntry(objectTitle: "<script>alert('xss')</script>");
|
||||||
|
var svc = new UserAccessHtmlExportService();
|
||||||
|
var html = svc.BuildHtml(new[] { entryWithScript });
|
||||||
|
|
||||||
|
// Raw script tag must not appear verbatim
|
||||||
|
Assert.DoesNotContain("<script>alert", html);
|
||||||
|
// Encoded form must be present
|
||||||
|
Assert.Contains("<script>", html);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Moq;
|
||||||
|
using SharepointToolbox.Core.Messages;
|
||||||
|
using SharepointToolbox.Core.Models;
|
||||||
|
using SharepointToolbox.Services;
|
||||||
|
using SharepointToolbox.ViewModels;
|
||||||
|
using SharepointToolbox.ViewModels.Tabs;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.Tests.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unit tests for UserAccessAuditViewModel (Phase 7 Plan 08).
|
||||||
|
/// Verifies: AuditUsersAsync invocation, results population, summary properties,
|
||||||
|
/// tenant switch reset, global sites message, override guard, CanExport state.
|
||||||
|
/// </summary>
|
||||||
|
public class UserAccessAuditViewModelTests
|
||||||
|
{
|
||||||
|
// ── Reset messenger between tests ─────────────────────────────────────────
|
||||||
|
|
||||||
|
public UserAccessAuditViewModelTests()
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helper factories ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static UserAccessEntry MakeEntry(
|
||||||
|
string userLogin = "alice@contoso.com",
|
||||||
|
string siteUrl = "https://contoso.sharepoint.com",
|
||||||
|
bool isHighPrivilege = false) =>
|
||||||
|
new("Alice", userLogin, siteUrl, "Contoso", "List", "Docs",
|
||||||
|
siteUrl + "/Docs", "Read", AccessType.Direct, "Direct Permissions",
|
||||||
|
isHighPrivilege, false);
|
||||||
|
|
||||||
|
private static GraphUserResult MakeUser(
|
||||||
|
string display = "Alice Smith",
|
||||||
|
string upn = "alice@contoso.com") =>
|
||||||
|
new(display, upn, upn);
|
||||||
|
|
||||||
|
/// <summary>Creates a ViewModel wired with a mock IUserAccessAuditService.</summary>
|
||||||
|
private static (UserAccessAuditViewModel vm, Mock<IUserAccessAuditService> auditMock)
|
||||||
|
CreateViewModel(IReadOnlyList<UserAccessEntry>? auditResult = null)
|
||||||
|
{
|
||||||
|
var mockAudit = new Mock<IUserAccessAuditService>();
|
||||||
|
mockAudit
|
||||||
|
.Setup(s => s.AuditUsersAsync(
|
||||||
|
It.IsAny<ISessionManager>(),
|
||||||
|
It.IsAny<IReadOnlyList<string>>(),
|
||||||
|
It.IsAny<IReadOnlyList<SiteInfo>>(),
|
||||||
|
It.IsAny<ScanOptions>(),
|
||||||
|
It.IsAny<IProgress<OperationProgress>>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(auditResult ?? Array.Empty<UserAccessEntry>());
|
||||||
|
|
||||||
|
var mockGraph = new Mock<IGraphUserSearchService>();
|
||||||
|
var mockSession = new Mock<ISessionManager>();
|
||||||
|
|
||||||
|
var vm = new UserAccessAuditViewModel(
|
||||||
|
mockAudit.Object,
|
||||||
|
mockGraph.Object,
|
||||||
|
mockSession.Object,
|
||||||
|
NullLogger<FeatureViewModelBase>.Instance);
|
||||||
|
|
||||||
|
return (vm, mockAudit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 1: RunOperation calls AuditUsersAsync ────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunOperation_calls_AuditUsersAsync()
|
||||||
|
{
|
||||||
|
var (vm, auditMock) = CreateViewModel();
|
||||||
|
|
||||||
|
vm.SelectedUsers.Add(MakeUser());
|
||||||
|
vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso"));
|
||||||
|
|
||||||
|
await vm.TestRunOperationAsync(CancellationToken.None, new Progress<OperationProgress>());
|
||||||
|
|
||||||
|
auditMock.Verify(
|
||||||
|
s => s.AuditUsersAsync(
|
||||||
|
It.IsAny<ISessionManager>(),
|
||||||
|
It.IsAny<IReadOnlyList<string>>(),
|
||||||
|
It.IsAny<IReadOnlyList<SiteInfo>>(),
|
||||||
|
It.IsAny<ScanOptions>(),
|
||||||
|
It.IsAny<IProgress<OperationProgress>>(),
|
||||||
|
It.IsAny<CancellationToken>()),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 2: RunOperation populates Results ────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunOperation_populates_results()
|
||||||
|
{
|
||||||
|
var entries = new List<UserAccessEntry>
|
||||||
|
{
|
||||||
|
MakeEntry(),
|
||||||
|
MakeEntry(userLogin: "bob@contoso.com")
|
||||||
|
};
|
||||||
|
|
||||||
|
var (vm, _) = CreateViewModel(entries);
|
||||||
|
|
||||||
|
vm.SelectedUsers.Add(MakeUser());
|
||||||
|
vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso"));
|
||||||
|
|
||||||
|
await vm.TestRunOperationAsync(CancellationToken.None, new Progress<OperationProgress>());
|
||||||
|
|
||||||
|
Assert.Equal(2, vm.Results.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 3: RunOperation updates summary properties ───────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunOperation_updates_summary_properties()
|
||||||
|
{
|
||||||
|
var entries = new List<UserAccessEntry>
|
||||||
|
{
|
||||||
|
MakeEntry(userLogin: "alice@contoso.com", siteUrl: "https://contoso.sharepoint.com/sites/s1", isHighPrivilege: true),
|
||||||
|
MakeEntry(userLogin: "alice@contoso.com", siteUrl: "https://contoso.sharepoint.com/sites/s2", isHighPrivilege: false),
|
||||||
|
MakeEntry(userLogin: "bob@contoso.com", siteUrl: "https://contoso.sharepoint.com/sites/s1", isHighPrivilege: true)
|
||||||
|
};
|
||||||
|
|
||||||
|
var (vm, _) = CreateViewModel(entries);
|
||||||
|
|
||||||
|
vm.SelectedUsers.Add(MakeUser());
|
||||||
|
vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso"));
|
||||||
|
|
||||||
|
await vm.TestRunOperationAsync(CancellationToken.None, new Progress<OperationProgress>());
|
||||||
|
|
||||||
|
Assert.Equal(3, vm.TotalAccessCount);
|
||||||
|
Assert.Equal(2, vm.SitesCount);
|
||||||
|
Assert.Equal(2, vm.HighPrivilegeCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 4: OnTenantSwitched resets state ─────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task OnTenantSwitched_resets_state()
|
||||||
|
{
|
||||||
|
var entries = new List<UserAccessEntry> { MakeEntry() };
|
||||||
|
var (vm, _) = CreateViewModel(entries);
|
||||||
|
|
||||||
|
// Populate state
|
||||||
|
vm.SelectedUsers.Add(MakeUser());
|
||||||
|
vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso"));
|
||||||
|
await vm.TestRunOperationAsync(CancellationToken.None, new Progress<OperationProgress>());
|
||||||
|
Assert.NotEmpty(vm.Results);
|
||||||
|
Assert.NotEmpty(vm.SelectedUsers);
|
||||||
|
|
||||||
|
// Act: send TenantSwitchedMessage
|
||||||
|
var newProfile = new TenantProfile
|
||||||
|
{
|
||||||
|
Name = "NewTenant",
|
||||||
|
TenantUrl = "https://newtenant.sharepoint.com",
|
||||||
|
ClientId = "new-client-id"
|
||||||
|
};
|
||||||
|
WeakReferenceMessenger.Default.Send(new TenantSwitchedMessage(newProfile));
|
||||||
|
|
||||||
|
// Assert: state cleared
|
||||||
|
Assert.Empty(vm.Results);
|
||||||
|
Assert.Empty(vm.SelectedUsers);
|
||||||
|
Assert.Empty(vm.SelectedSites);
|
||||||
|
Assert.Empty(vm.FilterText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 5: GlobalSitesChanged updates SelectedSites ─────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnGlobalSitesChanged_updates_selected_sites()
|
||||||
|
{
|
||||||
|
var (vm, _) = CreateViewModel();
|
||||||
|
var sites = new List<SiteInfo>
|
||||||
|
{
|
||||||
|
new("https://contoso.sharepoint.com/sites/hr", "HR"),
|
||||||
|
new("https://contoso.sharepoint.com/sites/finance", "Finance")
|
||||||
|
}.AsReadOnly();
|
||||||
|
|
||||||
|
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites));
|
||||||
|
|
||||||
|
Assert.Equal(2, vm.SelectedSites.Count);
|
||||||
|
Assert.Equal("https://contoso.sharepoint.com/sites/hr", vm.SelectedSites[0].Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 6: GlobalSitesChanged skipped when override active ──────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnGlobalSitesChanged_skipped_when_override()
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
vm.SelectedSites.Add(localSite);
|
||||||
|
|
||||||
|
var field = typeof(UserAccessAuditViewModel)
|
||||||
|
.GetField("_hasLocalSiteOverride",
|
||||||
|
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||||
|
field!.SetValue(vm, true);
|
||||||
|
|
||||||
|
// Act: send global sites message
|
||||||
|
var sites = new List<SiteInfo>
|
||||||
|
{
|
||||||
|
new("https://contoso.sharepoint.com/sites/global1", "Global1")
|
||||||
|
}.AsReadOnly();
|
||||||
|
WeakReferenceMessenger.Default.Send(new GlobalSitesChangedMessage(sites));
|
||||||
|
|
||||||
|
// Assert: SelectedSites unchanged (override prevented update)
|
||||||
|
Assert.Single(vm.SelectedSites);
|
||||||
|
Assert.Equal("https://contoso.sharepoint.com/sites/local", vm.SelectedSites[0].Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 7: CanExport false when no results ───────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanExport_false_when_no_results()
|
||||||
|
{
|
||||||
|
var (vm, _) = CreateViewModel();
|
||||||
|
|
||||||
|
// Results is empty by default
|
||||||
|
Assert.Empty(vm.Results);
|
||||||
|
Assert.False(vm.ExportCsvCommand.CanExecute(null));
|
||||||
|
Assert.False(vm.ExportHtmlCommand.CanExecute(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test 8: CanExport true when has results ───────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanExport_true_when_has_results()
|
||||||
|
{
|
||||||
|
var entries = new List<UserAccessEntry> { MakeEntry() };
|
||||||
|
var (vm, _) = CreateViewModel(entries);
|
||||||
|
|
||||||
|
vm.SelectedUsers.Add(MakeUser());
|
||||||
|
vm.SelectedSites.Add(new SiteInfo("https://contoso.sharepoint.com", "Contoso"));
|
||||||
|
|
||||||
|
await vm.TestRunOperationAsync(CancellationToken.None, new Progress<OperationProgress>());
|
||||||
|
|
||||||
|
Assert.NotEmpty(vm.Results);
|
||||||
|
Assert.True(vm.ExportCsvCommand.CanExecute(null));
|
||||||
|
Assert.True(vm.ExportHtmlCommand.CanExecute(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user