feat(02-03): implement ISiteListService and SiteListService with admin URL derivation
- SiteInfo record added to Core/Models - ISiteListService interface with GetSitesAsync signature - SiteListService derives admin URL via Regex, connects via SessionManager - Filters to Active sites only, excludes OneDrive personal (-my.sharepoint.com) - Access denied ServerException wrapped as InvalidOperationException with actionable message - DeriveAdminUrl marked internal static for unit testability - InternalsVisibleTo added to AssemblyInfo.cs to expose internal to test project - 2 DeriveAdminUrl tests pass; full suite: 53 pass, 4 skip, 0 fail
This commit is contained in:
13
SharepointToolbox/AssemblyInfo.cs
Normal file
13
SharepointToolbox/AssemblyInfo.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("SharepointToolbox.Tests")]
|
||||||
|
|
||||||
|
[assembly:ThemeInfo(
|
||||||
|
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// or application resource dictionaries)
|
||||||
|
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// app, or any theme specific resource dictionaries)
|
||||||
|
)]
|
||||||
3
SharepointToolbox/Core/Models/SiteInfo.cs
Normal file
3
SharepointToolbox/Core/Models/SiteInfo.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace SharepointToolbox.Core.Models;
|
||||||
|
|
||||||
|
public record SiteInfo(string Url, string Title);
|
||||||
11
SharepointToolbox/Services/ISiteListService.cs
Normal file
11
SharepointToolbox/Services/ISiteListService.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using SharepointToolbox.Core.Models;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.Services;
|
||||||
|
|
||||||
|
public interface ISiteListService
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<SiteInfo>> GetSitesAsync(
|
||||||
|
TenantProfile profile,
|
||||||
|
IProgress<OperationProgress> progress,
|
||||||
|
CancellationToken ct);
|
||||||
|
}
|
||||||
69
SharepointToolbox/Services/SiteListService.cs
Normal file
69
SharepointToolbox/Services/SiteListService.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Online.SharePoint.TenantAdministration;
|
||||||
|
using Microsoft.SharePoint.Client;
|
||||||
|
using SharepointToolbox.Core.Models;
|
||||||
|
|
||||||
|
namespace SharepointToolbox.Services;
|
||||||
|
|
||||||
|
public class SiteListService : ISiteListService
|
||||||
|
{
|
||||||
|
private readonly SessionManager _sessionManager;
|
||||||
|
|
||||||
|
public SiteListService(SessionManager sessionManager)
|
||||||
|
{
|
||||||
|
_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SiteInfo>> GetSitesAsync(
|
||||||
|
TenantProfile profile,
|
||||||
|
IProgress<OperationProgress> progress,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
progress.Report(OperationProgress.Indeterminate("Loading sites..."));
|
||||||
|
|
||||||
|
var adminUrl = DeriveAdminUrl(profile.TenantUrl);
|
||||||
|
var adminProfile = new TenantProfile
|
||||||
|
{
|
||||||
|
Name = profile.Name,
|
||||||
|
TenantUrl = adminUrl,
|
||||||
|
ClientId = profile.ClientId
|
||||||
|
};
|
||||||
|
|
||||||
|
ClientContext adminCtx;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
adminCtx = await _sessionManager.GetOrCreateContextAsync(adminProfile, ct);
|
||||||
|
}
|
||||||
|
catch (ServerException ex) when (ex.Message.Contains("Access denied", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Site listing requires SharePoint administrator permissions. Connect with an admin account.", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenant = new Tenant(adminCtx);
|
||||||
|
var siteProps = tenant.GetSitePropertiesFromSharePoint("", true);
|
||||||
|
adminCtx.Load(siteProps);
|
||||||
|
await adminCtx.ExecuteQueryAsync();
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
return siteProps
|
||||||
|
.Where(s => s.Status == "Active"
|
||||||
|
&& !s.Url.Contains("-my.sharepoint.com", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(s => new SiteInfo(s.Url, s.Title))
|
||||||
|
.OrderBy(s => s.Url)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string DeriveAdminUrl(string tenantUrl)
|
||||||
|
=> Regex.Replace(tenantUrl.TrimEnd('/'),
|
||||||
|
@"(https://[^.]+)(\.sharepoint\.com)",
|
||||||
|
"$1-admin$2",
|
||||||
|
RegexOptions.IgnoreCase);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user