using SharepointToolbox.Core.Models;
using SharepointToolbox.Services.Export;
namespace SharepointToolbox.Tests.Services.Export;
///
/// Tests for PERM-06: HTML export output.
///
public class HtmlExportServiceTests
{
private static PermissionEntry MakeEntry(
string users, string userLogins,
string url = "https://contoso.sharepoint.com/sites/A",
string principalType = "User") =>
new("Web", "Site A", url, true, users, userLogins, "Read", "Direct Permissions", principalType);
private static ReportBranding MakeBranding(bool msp = true, bool client = false)
{
var mspLogo = msp ? new LogoData { Base64 = "bXNw", MimeType = "image/png" } : null;
var clientLogo = client ? new LogoData { Base64 = "Y2xpZW50", MimeType = "image/jpeg" } : null;
return new ReportBranding(mspLogo, clientLogo);
}
private static IReadOnlyDictionary> MakeGroupMembers(
string groupName, params ResolvedMember[] members) =>
new Dictionary>(StringComparer.OrdinalIgnoreCase)
{
[groupName] = members.ToList()
};
[Fact]
public void BuildHtml_WithKnownEntries_ContainsUserNames()
{
var entry = MakeEntry("Bob Smith", "bob@contoso.com");
var svc = new HtmlExportService();
var html = svc.BuildHtml(new[] { entry });
Assert.Contains("Bob Smith", html);
}
[Fact]
public void BuildHtml_WithEmptyList_ReturnsValidHtml()
{
var svc = new HtmlExportService();
var html = svc.BuildHtml(Array.Empty());
// Must be non-empty well-formed HTML even with no data rows
Assert.NotEmpty(html);
Assert.Contains("Payroll", html);
}
[Fact]
public void BuildHtml_WithSharingLinkTarget_RendersLinkTypeBadgeAndTarget()
{
var entry = new PermissionEntry(
ObjectType: "Folder",
Title: "Reports",
Url: "https://contoso.sharepoint.com/sites/HR/Shared%20Documents/Reports",
HasUniquePermissions: true,
Users: "SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.OrganizationEdit.64c27910-66ea-421d-b0f0-8f0f72dcfaf6",
UserLogins: "SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.OrganizationEdit.64c27910-66ea-421d-b0f0-8f0f72dcfaf6",
PermissionLevels: "Contribute",
GrantedThrough: "SharePoint Group: SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.OrganizationEdit.64c27910-66ea-421d-b0f0-8f0f72dcfaf6",
PrincipalType: "SharePointGroup",
TargetUrl: "https://contoso.sharepoint.com/sites/HR/Shared%20Documents/Reports/Q4.xlsx",
TargetLabel: "Q4.xlsx",
SharingLinkType: "OrganizationEdit");
var svc = new HtmlExportService();
var html = svc.BuildHtml(new[] { entry });
// Friendly label, raw code preserved as tooltip
Assert.Contains("Org link", html);
Assert.Contains("Edit", html);
Assert.Contains("title=\"OrganizationEdit\"", html);
Assert.Contains("Q4.xlsx", html);
Assert.Contains("https://contoso.sharepoint.com/sites/HR/Shared%20Documents/Reports/Q4.xlsx", html);
}
[Theory]
[InlineData("OrganizationView", "Org link", "View")]
[InlineData("AnonymousEdit", "Anyone", "Edit")]
[InlineData("Direct", "Specific people", null)]
[InlineData("Flexible", "Custom link", null)]
public void BuildHtml_FriendlyLinkLabel_ReplacesRawType(string raw, string expectedPart, string? expectedSecond)
{
var entry = new PermissionEntry(
ObjectType: "File", Title: "Doc.docx",
Url: "https://contoso.sharepoint.com/sites/HR/Docs/Doc.docx",
HasUniquePermissions: true,
Users: $"SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.{raw}.64c27910-66ea-421d-b0f0-8f0f72dcfaf6",
UserLogins: $"SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.{raw}.64c27910-66ea-421d-b0f0-8f0f72dcfaf6",
PermissionLevels: "Contribute",
GrantedThrough: $"SharePoint Group: SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.{raw}.64c27910-66ea-421d-b0f0-8f0f72dcfaf6",
PrincipalType: "SharePointGroup",
TargetUrl: "https://contoso.sharepoint.com/sites/HR/Docs/Doc.docx",
TargetLabel: "Doc.docx",
SharingLinkType: raw);
var svc = new HtmlExportService();
var html = svc.BuildHtml(new[] { entry }, null, null, hideSystemGroupRaw: true);
Assert.Contains(expectedPart, html);
if (expectedSecond is not null)
Assert.Contains(expectedSecond, html);
Assert.Contains($"title=\"{raw}\"", html);
}
[Fact]
public void BuildHtml_WithSharingLinkAndHideRaw_OmitsRawGroupString()
{
var entry = new PermissionEntry(
ObjectType: "File", Title: "Q4.xlsx",
Url: "https://contoso.sharepoint.com/sites/HR/Docs/Q4.xlsx",
HasUniquePermissions: true,
Users: "SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.OrganizationEdit.64c27910-66ea-421d-b0f0-8f0f72dcfaf6",
UserLogins: "SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.OrganizationEdit.64c27910-66ea-421d-b0f0-8f0f72dcfaf6",
PermissionLevels: "Contribute",
GrantedThrough: "SharePoint Group: SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.OrganizationEdit.64c27910-66ea-421d-b0f0-8f0f72dcfaf6",
PrincipalType: "SharePointGroup",
TargetUrl: "https://contoso.sharepoint.com/sites/HR/Docs/Q4.xlsx",
TargetLabel: "Q4.xlsx",
SharingLinkType: "OrganizationEdit");
var svc = new HtmlExportService();
var html = svc.BuildHtml(new[] { entry }, null, null, hideSystemGroupRaw: true);
// Raw "SharingLinks.{guid}..." text suppressed when target resolved + flag on
Assert.DoesNotContain("SharingLinks.e686221e", html);
// Target link still rendered
Assert.Contains("Q4.xlsx", html);
Assert.Contains("OrganizationEdit", html);
}
[Fact]
public void BuildHtml_WithSharingLinkButNoTargetAndHideRaw_KeepsRawAsFallback()
{
// Target resolution failed (deleted item) — flag on, but no TargetUrl → keep raw text.
var entry = new PermissionEntry(
ObjectType: "File", Title: "Q4.xlsx",
Url: "https://contoso.sharepoint.com/sites/HR/Docs/Q4.xlsx",
HasUniquePermissions: true,
Users: "SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.OrganizationEdit.64c27910-66ea-421d-b0f0-8f0f72dcfaf6",
UserLogins: "SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.OrganizationEdit.64c27910-66ea-421d-b0f0-8f0f72dcfaf6",
PermissionLevels: "Contribute",
GrantedThrough: "SharePoint Group: SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.OrganizationEdit.64c27910-66ea-421d-b0f0-8f0f72dcfaf6",
PrincipalType: "SharePointGroup",
SharingLinkType: "OrganizationEdit");
var svc = new HtmlExportService();
var html = svc.BuildHtml(new[] { entry }, null, null, hideSystemGroupRaw: true);
Assert.Contains("SharingLinks.e686221e", html);
}
[Fact]
public void BuildHtml_WithoutTarget_RendersGrantedThroughTextOnly()
{
// Standard SP group with no resolved target — no extra link should appear.
var entry = MakeEntry("Owners", "ownersgroup", principalType: "SharePointGroup");
var svc = new HtmlExportService();
var html = svc.BuildHtml(new[] { entry });
Assert.DoesNotContain("→", html);
}
[Fact]
public void BuildHtml_Simplified_WithGroupMembers_RendersExpandablePill()
{
var innerEntry = new PermissionEntry("Web", "Site A", "https://contoso.sharepoint.com/sites/A",
true, "Site Members", "i:0#.f|membership|group@contoso.com", "Read", "Direct Permissions", "SharePointGroup");
var simplifiedEntry = new SimplifiedPermissionEntry(innerEntry);
var groupMembers = MakeGroupMembers("Site Members", new ResolvedMember("Bob", "bob@co.com"));
var svc = new HtmlExportService();
var html = svc.BuildHtml(new[] { simplifiedEntry }, null, groupMembers);
Assert.Contains("data-group-target=\"grpmem0\"", html);
Assert.Contains("class=\"user-pill group-expandable\"", html);
}
}