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