Added new feature : display the file/folder and link of a SharingLink object in the permissions reports.
This commit is contained in:
@@ -48,6 +48,54 @@ public class CsvExportServiceTests
|
||||
Assert.Contains("Object", csv);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCsv_WithResolvedTarget_IncludesTargetColumns()
|
||||
{
|
||||
var entry = new PermissionEntry(
|
||||
ObjectType: "Site",
|
||||
Title: "HR",
|
||||
Url: "https://contoso.sharepoint.com/sites/HR",
|
||||
HasUniquePermissions: true,
|
||||
Users: "Limited Access System Group For List b20e3b22-2b09-4c99-9ba4-37b42f3a12dc",
|
||||
UserLogins: "Limited Access System Group For List b20e3b22-2b09-4c99-9ba4-37b42f3a12dc",
|
||||
PermissionLevels: "Limited Access: Edit",
|
||||
GrantedThrough: "SharePoint Group: Limited Access System Group For List b20e3b22-2b09-4c99-9ba4-37b42f3a12dc",
|
||||
PrincipalType: "SharePointGroup",
|
||||
TargetUrl: "https://contoso.sharepoint.com/sites/HR/Lists/Payroll",
|
||||
TargetLabel: "Payroll");
|
||||
|
||||
var svc = new CsvExportService();
|
||||
var csv = svc.BuildCsv(new[] { entry });
|
||||
|
||||
Assert.Contains("TargetLabel", csv);
|
||||
Assert.Contains("TargetUrl", csv);
|
||||
Assert.Contains("Payroll", csv);
|
||||
Assert.Contains("https://contoso.sharepoint.com/sites/HR/Lists/Payroll", csv);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCsv_WithSharingLink_IncludesLinkType()
|
||||
{
|
||||
var entry = new PermissionEntry(
|
||||
ObjectType: "Folder", Title: "Reports", Url: "https://contoso.sharepoint.com/sites/HR/Docs",
|
||||
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 CsvExportService();
|
||||
var csv = svc.BuildCsv(new[] { entry });
|
||||
|
||||
Assert.Contains("SharingLinkType", csv);
|
||||
Assert.Contains("OrganizationEdit", csv);
|
||||
Assert.Contains("Q4.xlsx", csv);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildCsv_WithDuplicateUserPermissionGrantedThrough_MergesLocations()
|
||||
{
|
||||
|
||||
@@ -156,6 +156,146 @@ public class HtmlExportServiceTests
|
||||
Assert.Contains("getAttribute('data-group-target')", html);
|
||||
}
|
||||
|
||||
// ── System-group target rendering (Limited Access / SharingLinks) ────────
|
||||
|
||||
[Fact]
|
||||
public void BuildHtml_WithResolvedListTarget_RendersClickableLink()
|
||||
{
|
||||
var entry = new PermissionEntry(
|
||||
ObjectType: "Site",
|
||||
Title: "Contoso HR",
|
||||
Url: "https://contoso.sharepoint.com/sites/HR",
|
||||
HasUniquePermissions: true,
|
||||
Users: "Limited Access System Group For List b20e3b22-2b09-4c99-9ba4-37b42f3a12dc",
|
||||
UserLogins: "Limited Access System Group For List b20e3b22-2b09-4c99-9ba4-37b42f3a12dc",
|
||||
PermissionLevels: "Limited Access: Edit",
|
||||
GrantedThrough: "SharePoint Group: Limited Access System Group For List b20e3b22-2b09-4c99-9ba4-37b42f3a12dc",
|
||||
PrincipalType: "SharePointGroup",
|
||||
TargetUrl: "https://contoso.sharepoint.com/sites/HR/Lists/Payroll",
|
||||
TargetLabel: "Payroll");
|
||||
|
||||
var svc = new HtmlExportService();
|
||||
var html = svc.BuildHtml(new[] { entry });
|
||||
|
||||
Assert.Contains("https://contoso.sharepoint.com/sites/HR/Lists/Payroll", html);
|
||||
Assert.Contains(">Payroll</a>", 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()
|
||||
{
|
||||
|
||||
@@ -86,20 +86,15 @@ public class UserAccessCsvExportServiceTests
|
||||
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.
|
||||
// Header: Site, Object Type, Object, URL, Permission Level, Access Type,
|
||||
// Granted Through, TargetLabel, TargetUrl, SharingLinkType → 10 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);
|
||||
Assert.Equal(10, fields);
|
||||
}
|
||||
|
||||
// ── Test 5: WriteSingleFileAsync includes entries for all users ───────────
|
||||
@@ -190,8 +185,8 @@ public class UserAccessCsvExportServiceTests
|
||||
await svc.WriteSingleFileAsync(entries, tmpFile, CancellationToken.None, mergePermissions: true);
|
||||
var content = await File.ReadAllTextAsync(tmpFile);
|
||||
|
||||
// Header must contain consolidated columns
|
||||
Assert.Contains("\"User\",\"User Login\",\"Permission Level\",\"Access Type\",\"Granted Through\",\"Locations\",\"Location Count\"", content);
|
||||
// Header must contain consolidated columns (now includes Target* + SharingLinkType)
|
||||
Assert.Contains("\"User\",\"User Login\",\"Permission Level\",\"Access Type\",\"Granted Through\",\"TargetLabel\",\"TargetUrl\",\"SharingLinkType\",\"Locations\",\"Location Count\"", content);
|
||||
|
||||
// Alice's two entries merged — locations column contains both site titles
|
||||
Assert.Contains("Contoso", content);
|
||||
|
||||
@@ -3,7 +3,8 @@ using SharepointToolbox.Core.Helpers;
|
||||
namespace SharepointToolbox.Tests.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for PERM-03: external user detection and permission-level filtering.
|
||||
/// Tests for PERM-03: external user detection, permission-level filtering,
|
||||
/// and SharePoint system-group classification (Limited Access / SharingLinks).
|
||||
/// Pure static logic — runs immediately without stubs.
|
||||
/// </summary>
|
||||
public class PermissionEntryClassificationTests
|
||||
@@ -13,7 +14,6 @@ public class PermissionEntryClassificationTests
|
||||
[Fact]
|
||||
public void IsExternalUser_WithExtHashInLoginName_ReturnsTrue()
|
||||
{
|
||||
// B2B guest login names contain the literal "#EXT#" fragment
|
||||
Assert.True(PermissionEntryHelper.IsExternalUser("ext_user_domain.com#EXT#@contoso.onmicrosoft.com"));
|
||||
}
|
||||
|
||||
@@ -28,8 +28,6 @@ public class PermissionEntryClassificationTests
|
||||
[Fact]
|
||||
public void PermissionEntry_FiltersOutLimitedAccess_WhenOnlyPermissionIsLimitedAccess()
|
||||
{
|
||||
// A principal whose sole permission level is "Limited Access" should produce
|
||||
// an empty list after filtering — used to decide whether to include the entry.
|
||||
var result = PermissionEntryHelper.FilterPermissionLevels(new[] { "Limited Access" });
|
||||
Assert.Empty(result);
|
||||
}
|
||||
@@ -41,23 +39,100 @@ public class PermissionEntryClassificationTests
|
||||
Assert.Equal(new[] { "Contribute" }, result);
|
||||
}
|
||||
|
||||
// ── IsSharingLinksGroup ────────────────────────────────────────────────────
|
||||
// ── IsBareLimitedAccessSystemGroup ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsSharingLinksGroup_WithSharingLinksPrefix_ReturnsTrue()
|
||||
public void IsBareLimitedAccessSystemGroup_WithExactMatch_ReturnsTrue()
|
||||
{
|
||||
Assert.True(PermissionEntryHelper.IsSharingLinksGroup("SharingLinks.abc123.Edit"));
|
||||
Assert.True(PermissionEntryHelper.IsBareLimitedAccessSystemGroup("Limited Access System Group"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsSharingLinksGroup_WithLimitedAccessSystemGroup_ReturnsTrue()
|
||||
public void IsBareLimitedAccessSystemGroup_WithForWebVariant_ReturnsFalse()
|
||||
{
|
||||
Assert.True(PermissionEntryHelper.IsSharingLinksGroup("Limited Access System Group"));
|
||||
// The "For Web {guid}" variant is enriched, not filtered.
|
||||
Assert.False(PermissionEntryHelper.IsBareLimitedAccessSystemGroup(
|
||||
"Limited Access System Group For Web e55a4f7a-b132-4baa-bd96-c63d8dc1fc80"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsSharingLinksGroup_WithNormalGroup_ReturnsFalse()
|
||||
public void IsBareLimitedAccessSystemGroup_WithNormalGroup_ReturnsFalse()
|
||||
{
|
||||
Assert.False(PermissionEntryHelper.IsSharingLinksGroup("Owners"));
|
||||
Assert.False(PermissionEntryHelper.IsBareLimitedAccessSystemGroup("Owners"));
|
||||
}
|
||||
|
||||
// ── Classify ───────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Classify_WithNormalGroupTitle_ReturnsNone()
|
||||
{
|
||||
var c = PermissionEntryHelper.Classify("Site Owners");
|
||||
Assert.Equal(SystemGroupKind.None, c.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_WithBareLimitedAccess_ReturnsLimitedAccessBare()
|
||||
{
|
||||
var c = PermissionEntryHelper.Classify("Limited Access System Group");
|
||||
Assert.Equal(SystemGroupKind.LimitedAccessBare, c.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_WithLimitedAccessForWeb_ExtractsWebId()
|
||||
{
|
||||
var c = PermissionEntryHelper.Classify(
|
||||
"Limited Access System Group For Web e55a4f7a-b132-4baa-bd96-c63d8dc1fc80");
|
||||
Assert.Equal(SystemGroupKind.LimitedAccessWeb, c.Kind);
|
||||
Assert.Equal(Guid.Parse("e55a4f7a-b132-4baa-bd96-c63d8dc1fc80"), c.WebId);
|
||||
Assert.Null(c.ListId);
|
||||
Assert.Null(c.ItemUniqueId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_WithLimitedAccessForList_ExtractsListId()
|
||||
{
|
||||
var c = PermissionEntryHelper.Classify(
|
||||
"Limited Access System Group For List b20e3b22-2b09-4c99-9ba4-37b42f3a12dc");
|
||||
Assert.Equal(SystemGroupKind.LimitedAccessList, c.Kind);
|
||||
Assert.Equal(Guid.Parse("b20e3b22-2b09-4c99-9ba4-37b42f3a12dc"), c.ListId);
|
||||
Assert.Null(c.WebId);
|
||||
Assert.Null(c.ItemUniqueId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_WithSharingLink_ExtractsItemAndShareId()
|
||||
{
|
||||
var c = PermissionEntryHelper.Classify(
|
||||
"SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.OrganizationEdit.64c27910-66ea-421d-b0f0-8f0f72dcfaf6");
|
||||
Assert.Equal(SystemGroupKind.SharingLink, c.Kind);
|
||||
Assert.Equal(Guid.Parse("e686221e-d1cb-43c5-8c68-04aa7f90f329"), c.ItemUniqueId);
|
||||
Assert.Equal(Guid.Parse("64c27910-66ea-421d-b0f0-8f0f72dcfaf6"), c.ShareId);
|
||||
Assert.Equal("OrganizationEdit", c.LinkType);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.AnonymousView.64c27910-66ea-421d-b0f0-8f0f72dcfaf6", "AnonymousView")]
|
||||
[InlineData("SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.Flexible.64c27910-66ea-421d-b0f0-8f0f72dcfaf6", "Flexible")]
|
||||
[InlineData("SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.OrganizationView.64c27910-66ea-421d-b0f0-8f0f72dcfaf6", "OrganizationView")]
|
||||
public void Classify_WithVariousSharingLinkTypes_PreservesLinkType(string title, string expected)
|
||||
{
|
||||
var c = PermissionEntryHelper.Classify(title);
|
||||
Assert.Equal(SystemGroupKind.SharingLink, c.Kind);
|
||||
Assert.Equal(expected, c.LinkType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_WithMalformedSharingLink_ReturnsNone()
|
||||
{
|
||||
// Missing share GUID at the end
|
||||
var c = PermissionEntryHelper.Classify("SharingLinks.e686221e-d1cb-43c5-8c68-04aa7f90f329.Edit");
|
||||
Assert.Equal(SystemGroupKind.None, c.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Classify_WithEmptyString_ReturnsNone()
|
||||
{
|
||||
var c = PermissionEntryHelper.Classify("");
|
||||
Assert.Equal(SystemGroupKind.None, c.Kind);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user