feat(04-01): add Phase 4 models, interfaces, BulkOperationRunner, and test scaffolds

- Install CsvHelper 33.1.0 and Microsoft.Graph 5.74.0 (main + test projects)
- Add 14 core model/enum files (BulkOperationResult, BulkMemberRow, BulkSiteRow, TransferJob, FolderStructureRow, SiteTemplate, SiteTemplateOptions, TemplateLibraryInfo, TemplateFolderInfo, TemplatePermissionGroup, ConflictPolicy, TransferMode, CsvValidationRow)
- Add 6 service interfaces (IFileTransferService, IBulkMemberService, IBulkSiteService, ITemplateService, IFolderStructureService, ICsvValidationService)
- Add BulkOperationRunner with continue-on-error and cancellation support
- Add BulkResultCsvExportService stub (compile-ready)
- Add test scaffolds: BulkOperationRunnerTests (5 passing), BulkResultCsvExportServiceTests (2 passing), CsvValidationServiceTests (6 skipped), TemplateRepositoryTests (4 skipped)
This commit is contained in:
Dev
2026-04-03 09:53:05 +02:00
parent d73e50948d
commit 39deed9d8d
27 changed files with 604 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
using CsvHelper.Configuration.Attributes;
namespace SharepointToolbox.Core.Models;
public class BulkMemberRow
{
[Name("GroupName")]
public string GroupName { get; set; } = string.Empty;
[Name("GroupUrl")]
public string GroupUrl { get; set; } = string.Empty;
[Name("Email")]
public string Email { get; set; } = string.Empty;
[Name("Role")]
public string Role { get; set; } = string.Empty; // "Member" or "Owner"
}

View File

@@ -0,0 +1,35 @@
namespace SharepointToolbox.Core.Models;
public class BulkItemResult<T>
{
public T Item { get; }
public bool IsSuccess { get; }
public string? ErrorMessage { get; }
public DateTime Timestamp { get; }
private BulkItemResult(T item, bool success, string? error)
{
Item = item;
IsSuccess = success;
ErrorMessage = error;
Timestamp = DateTime.UtcNow;
}
public static BulkItemResult<T> Success(T item) => new(item, true, null);
public static BulkItemResult<T> Failed(T item, string error) => new(item, false, error);
}
public class BulkOperationSummary<T>
{
public IReadOnlyList<BulkItemResult<T>> Results { get; }
public int TotalCount => Results.Count;
public int SuccessCount => Results.Count(r => r.IsSuccess);
public int FailedCount => Results.Count(r => !r.IsSuccess);
public bool HasFailures => FailedCount > 0;
public IReadOnlyList<BulkItemResult<T>> FailedItems => Results.Where(r => !r.IsSuccess).ToList();
public BulkOperationSummary(IReadOnlyList<BulkItemResult<T>> results)
{
Results = results;
}
}

View File

@@ -0,0 +1,24 @@
using CsvHelper.Configuration.Attributes;
namespace SharepointToolbox.Core.Models;
public class BulkSiteRow
{
[Name("Name")]
public string Name { get; set; } = string.Empty;
[Name("Alias")]
public string Alias { get; set; } = string.Empty;
[Name("Type")]
public string Type { get; set; } = string.Empty; // "Team" or "Communication"
[Name("Template")]
public string Template { get; set; } = string.Empty;
[Name("Owners")]
public string Owners { get; set; } = string.Empty; // comma-separated emails
[Name("Members")]
public string Members { get; set; } = string.Empty; // comma-separated emails
}

View File

@@ -0,0 +1,8 @@
namespace SharepointToolbox.Core.Models;
public enum ConflictPolicy
{
Skip,
Overwrite,
Rename
}

View File

@@ -0,0 +1,25 @@
namespace SharepointToolbox.Core.Models;
public class CsvValidationRow<T>
{
public T? Record { get; }
public bool IsValid => Errors.Count == 0;
public List<string> Errors { get; }
public string? RawRecord { get; }
public CsvValidationRow(T record, List<string> errors)
{
Record = record;
Errors = errors;
}
private CsvValidationRow(string rawRecord, string parseError)
{
Record = default;
RawRecord = rawRecord;
Errors = new List<string> { parseError };
}
public static CsvValidationRow<T> ParseError(string? rawRecord, string error)
=> new(rawRecord ?? string.Empty, error);
}

View File

@@ -0,0 +1,28 @@
using CsvHelper.Configuration.Attributes;
namespace SharepointToolbox.Core.Models;
public class FolderStructureRow
{
[Name("Level1")]
public string Level1 { get; set; } = string.Empty;
[Name("Level2")]
public string Level2 { get; set; } = string.Empty;
[Name("Level3")]
public string Level3 { get; set; } = string.Empty;
[Name("Level4")]
public string Level4 { get; set; } = string.Empty;
/// <summary>
/// Builds the folder path from non-empty level values (e.g. "Admin/HR/Contracts").
/// </summary>
public string BuildPath()
{
var parts = new[] { Level1, Level2, Level3, Level4 }
.Where(s => !string.IsNullOrWhiteSpace(s));
return string.Join("/", parts);
}
}

View File

@@ -0,0 +1,27 @@
namespace SharepointToolbox.Core.Models;
public class SiteTemplate
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Name { get; set; } = string.Empty;
public string SourceUrl { get; set; } = string.Empty;
public DateTime CapturedAt { get; set; }
public string SiteType { get; set; } = string.Empty; // "Team" or "Communication"
public SiteTemplateOptions Options { get; set; } = new();
public TemplateSettings? Settings { get; set; }
public TemplateLogo? Logo { get; set; }
public List<TemplateLibraryInfo> Libraries { get; set; } = new();
public List<TemplatePermissionGroup> PermissionGroups { get; set; } = new();
}
public class TemplateSettings
{
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public int Language { get; set; }
}
public class TemplateLogo
{
public string LogoUrl { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,10 @@
namespace SharepointToolbox.Core.Models;
public class SiteTemplateOptions
{
public bool CaptureLibraries { get; set; } = true;
public bool CaptureFolders { get; set; } = true;
public bool CapturePermissionGroups { get; set; } = true;
public bool CaptureLogo { get; set; } = true;
public bool CaptureSettings { get; set; } = true;
}

View File

@@ -0,0 +1,8 @@
namespace SharepointToolbox.Core.Models;
public class TemplateFolderInfo
{
public string Name { get; set; } = string.Empty;
public string RelativePath { get; set; } = string.Empty;
public List<TemplateFolderInfo> Children { get; set; } = new();
}

View File

@@ -0,0 +1,9 @@
namespace SharepointToolbox.Core.Models;
public class TemplateLibraryInfo
{
public string Name { get; set; } = string.Empty;
public string BaseType { get; set; } = string.Empty; // "DocumentLibrary", "GenericList"
public int BaseTemplate { get; set; }
public List<TemplateFolderInfo> Folders { get; set; } = new();
}

View File

@@ -0,0 +1,8 @@
namespace SharepointToolbox.Core.Models;
public class TemplatePermissionGroup
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public List<string> RoleDefinitions { get; set; } = new(); // e.g. "Full Control", "Contribute"
}

View File

@@ -0,0 +1,13 @@
namespace SharepointToolbox.Core.Models;
public class TransferJob
{
public string SourceSiteUrl { get; set; } = string.Empty;
public string SourceLibrary { get; set; } = string.Empty;
public string SourceFolderPath { get; set; } = string.Empty; // relative within library
public string DestinationSiteUrl { get; set; } = string.Empty;
public string DestinationLibrary { get; set; } = string.Empty;
public string DestinationFolderPath { get; set; } = string.Empty;
public TransferMode Mode { get; set; } = TransferMode.Copy;
public ConflictPolicy ConflictPolicy { get; set; } = ConflictPolicy.Skip;
}

View File

@@ -0,0 +1,7 @@
namespace SharepointToolbox.Core.Models;
public enum TransferMode
{
Copy,
Move
}